mirror of
https://github.com/simstudioai/sim.git
synced 2026-10-16 06:39:56 +08:00
improvement(platform): remove tour, simplify sidebar/header, drop loading skeletons (#4354)
* improvement(platform): workspace UI/UX overhaul + integrations catalog Rework the workspace around the AI-workspace model: a Mothership home, a top-level Skills route, connected-credential and integration-detail pages, and a polished sidebar/settings surface. Replace the notifications store with a unified toast system (provider-level dismiss/pause, countdown ring). Integrations & catalog: - Add a BlockMeta layer (tags + catalog templates) scoped to catalog-visible integrations; every catalog integration carries >=7 grounded templates. - Rework the taxonomy: each block declares category tools|blocks|triggers. 3rd-party services are 'tools'; first-party primitives (postgres, mysql, knowledge, file, search, stt/tts, image/video generators, thinking, etc.) are 'blocks'. Versioned blocks follow the upgrade paradigm (old hidden, latest in toolbar/docs). - Generate integrations.json + tool docs canonically from block configs. Architecture & cleanup: - Consolidate block data extraction behind a single latest-version strategy (getCanonicalBlocksByCategory; version-consistent getBlockMeta). - Unify version-suffix handling in @sim/utils/string (stripVersionSuffix / isVersionedType, with tests); registry, generate-docs, tools/utils, and integrations all route through it. - Repair latent broken barrels, remove dead code, fix BlockMeta-related type errors and 5 broken docs links. Behavior-preserving for block execution and the toolbar's tool/block listing. * refactor(platform): remove forms, templates, and creators features Remove three standalone features and their supporting code: - Forms: form-deployment pages, API routes, execution path, and docs. - Templates: the template gallery (landing + workspace) and template APIs. - Creators: creator-profile routes and contracts. Add a super-user permissions module (lib/permissions/super-user) and an organizations API contract; update the audit/db/testing packages, billing, and the session/theme providers accordingly. * test(workflows): update archiveWorkflow update count after forms removal The forms feature was removed, dropping the form-table update from archiveWorkflow. Update the stale assertion from 8 to 7 tx.update calls. * upgrade * improvement(knowledge): polish tag filter dropdowns (#4816) * improvement(logs): object storage backed tracespans (#4787) * improvement(logs): obj storage backed tracespans * fix storage write context * fix tests * address comments * address comments * chore(db): remove migration 0219 to regenerate after staging merge Drops the 0219_robust_shard SQL, its snapshot, and the journal entry so the trace-spans/cost schema migration can be regenerated on top of the latest staging migration chain (avoids a number collision with staging's migrations). Co-authored-by: Cursor <cursoragent@cursor.com> * improvement(billing): accurate per-member usage via shared ledger helper Per-member/per-user usage in the org-member routes now adds the usage_log ledger to the currentPeriodCost baseline (which is no longer incremented), via a shared getOrgMemberLedgerByUser helper to avoid repeating the subscription→period→ledger lookup across the admin and member-facing routes. Co-authored-by: Cursor <cursoragent@cursor.com> * regen migrations * update migration * address comments * more code cleanup * incorrect type cast --------- Co-authored-by: Cursor <cursoragent@cursor.com> * improvement(providers): harden OpenAI-compatible providers + add tests (#4796) * improvement(providers): harden OpenAI-compatible providers + add tests * fix(vllm): let tool-loop errors propagate instead of returning silent partial success * fix(litellm): force tool_choice 'none' on final structured-output call The deferred final call used tool_choice 'auto', so the model could emit another tool_calls round instead of the structured answer, leaving content stale. Use 'none' (matching vLLM/Fireworks) on both the streaming and non-streaming final calls so the model must return the structured response. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(providers/ollama): drop tools from post-tool streaming call Ollama ignores tool_choice (not in its supported fields), so vLLM/Fireworks' tool_choice:'none' guard is a no-op here. Omit tools from the final streaming payload instead so the summarization turn can't emit dropped tool calls. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(litellm): spread payload into deferred final call so reasoning_effort carries over The non-streaming deferred finalPayload hand-picked fields and dropped reasoning_effort (and any future payload field), diverging from the streaming path which spreads ...payload. Spread payload here too for consistency. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(providers/ollama): restore enrichment TSDoc block Keeps parity with sibling Chat Completions providers (cerebras/mistral/xai). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(fireworks): restore TSDoc on utils helpers Restore the TSDoc blocks on supportsNativeStructuredOutputs, createReadableStreamFromOpenAIStream, and checkForForcedToolUsage — TSDoc is the codebase documentation standard and should not have been stripped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(litellm): remove inline rationale comments (codebase uses TSDoc) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(providers/ollama): drop orphaned enrichment TSDoc The block documented a function that now lives in trace-enrichment.ts, so it documents nothing in this file. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * chore(copilot): deprecate mcp server (#4797) * chore(copilot): deprecate mcp * update error codes * deprecate copilot api v1 route * feat(integrations): hosted API keys for Findymail, Prospeo, and Wiza (#4777) * feat(integrations): hosted API keys for Findymail, Prospeo, and Wiza Add hosted-key support across all credit-consuming Findymail, Prospeo, and Wiza operations so Sim provides the key when a workspace has not brought its own. Register the three BYOK providers, consolidate Wiza's two-step reveal into a single polling wiza_individual_reveal op, and hide the API key field on hosted Sim for hosted operations. * fix(integrations): harden Wiza reveal polling, soften enrichment getCost guards Address Greptile + Cursor Bugbot review on #4777: return explicit failures from the Wiza individual_reveal poller instead of throwing (thrown errors were swallowed into a false queued success), short-circuit when the initial reveal is already terminal, tolerate transient 5xx/429 during polling, and return 0 (not throw) from Findymail getCost when the contacts/employees array is absent. * chore(integrations): biome formatting after wiza merge resolution * fix(wiza): type isTerminalReveal param structurally for next build typecheck * feat(enrichments): add Findymail, Prospeo, Wiza to work-email waterfall * feat(enrichments): add Wiza + Prospeo phone reveal to phone-number waterfall * feat(enrichments): opportunistic identifiers + LinkedIn URL input across work-email & phone cascades * fix(tables): reduce column header chevron size and fix sidebar shadow bleed (#4800) * feat(slack): add install + privacy section to integration landing page (#4799) * feat(slack): add install + privacy section to integration landing page Adds a hand-authored, slug-keyed landing-content module (separate from the generated integrations.json so it survives regeneration) and renders an install walkthrough + privacy-policy link on integration pages when present. Also refreshes generated docs (data-enrichment entry, icon mappings, tool mdx). * fix(landing): render privacy section independently, align CTA analytics label * docs(landing): clarify the Slack install button is behind sign-in * refactor(landing): bake integration landing content into generated json via docs-gen Moves landing content (install walkthrough + privacy) out of a render-time augment and into the generation pipeline: generate-docs reads the pure-data content map and writes landingContent into integrations.json, so the page reads a single source (integration.landingContent). Canonical types live in integrations/data/types.ts. * improvement(enrichments): align enrichments sidebar with design system (#4801) * improvement(enrichments): align enrichments sidebar with design system * fix(enrichments): consistent close button pattern and fix url link hover * fix(misc): upgrade path change for new better-auth version, billing issue for workflow block agent usage (#4803) * fix(misc): upgrade path change for new better-auth version, double-billing for workflow block agent usage * fail loudly if stripe sub id missing * fix(copilot): seq migration (#4804) * chore(db): drop redundant idx_webhook_on_workflow_id_block_id index (#4809) Removed because (workflow_id, block_id) is a left-prefix of idx_webhook_on_workflow_id_block_id_updated_at_desc, which fully covers it. The dropped index was non-unique and enforced no constraint. * perf(copilot): read chat transcripts from copilot_messages (R+1 cutover) (#4808) * perf(copilot): read chat transcripts from copilot_messages, not JSONB Flip user-facing chat reads from the legacy copilot_chats.messages JSONB array (5.7GB, 99% TOAST) to the normalized copilot_messages table via a new loadCopilotChatMessages helper ordered by seq NULLS LAST, created_at, id — the verified canonical order. Both chat-detail getters (getAccessibleCopilotChat, getAccessibleCopilotChatWithMessages) now drop the messages column from their metadata select (no more whole-array detoast on every load) and assemble the transcript from the table after authorization. This cascades to the copilot + mothership GET endpoints and to resolveOrCreateChat's conversationHistory (the LLM payload). The normalize/effective-transcript pipeline is source-agnostic (copilot_messages.content == a JSONB array element), so transcripts are byte-identical. Dual-write and the JSONB column stay in place as the internal-logic source and fallback; removing JSONB writes is a later step. Prod integrity verified before cutover: 0 messages missing, 0 NULL-seq, 0 dup keys/seq, 0 orphans, order-parity vs JSONB = 0 mismatches. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(copilot): cover auth-deny on a found row skips the messages query Address PR review: exercise the `if (!authorized) return null` contract — when the chat row exists but authorization fails, the getter returns null and never issues the copilot_messages read. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * fix(tables): right-align run/stop in embedded toolbar; workflow cells format like normal cells (#4806) * fix(tables): right-align run/stop in the embedded table toolbar Add a right-aligned `trailing` slot to ResourceOptionsBar and move the embedded mothership table's run/stop control into it, so Filter + Sort stay left-aligned and run/stop sits opposite on the right. No-op for the search-bearing consumers (logs, resource list), which don't pass `trailing`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(tables): workflow-output cells format values like normal cells Workflow-output columns short-circuited in resolveCellRender and rendered their value as plain text, so a sim-resource URL / external URL / JSON / date produced by a workflow never got the chip, favicon link, or typed formatting a normal cell gets. Factor value formatting into a shared `resolveValueKind` helper used by both the workflow-value branch and the plain-cell branch; the workflow branch keeps the typewriter reveal for plain streaming text via a `typewriter` flag. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(tables): detect resource/URL links on workflow output regardless of column type Workflow output columns default to `json` (columnTypeForLeaf), so routing their values through the type-based formatter (a) gated chip/URL promotion behind `column.type === 'string'` — a URL produced by a json-typed output never became a chip — and (b) JSON.stringify'd plain string values, adding quotes and losing the typewriter reveal. Detect links (sim-resource chip / favicon URL) on the value string directly for workflow outputs, falling back to the plain `value` kind; plain cells keep the type-based formatting. Addresses Greptile P2 on #4806. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(icons): repair broken integration icon rendering (#4810) * fix(icons): repair broken integration icon rendering Two distinct bugs left integration icons broken on the /integrations page (visible at 32-40px, hidden at the toolbar's 16px): 1. Corrupted SVG paths (Notion, Greptile, Granola, Calendly, Grafana, Bedrock): over-minified data dropped elliptical-arc flag digits (e.g. `A1 1 0 5.9 7` instead of `A1 1 0 0 0 5.9 7`); Granola's cubic stream was truncated. Browsers abort path parsing at the first invalid arc flag, so each rendered as a fragment or blank. Replaced with correct path data from canonical sources, preserving each icon's existing fill/gradient and bgColor. 2. Invisible glyph (Bright Data): its icon uses fill='currentColor' but bgColor was '#FFFFFF', and every surface forces text-white on the glyph - white-on-white. Changed bgColor to Bright Data's brand blue (#3d7ffc) so the white glyph reads, matching the white-glyph-on-brand-chip convention. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(icons): restore Calendly dual-tone brand colors Addresses review feedback: the previous fix replaced the broken Calendly icon with a monochrome #006BFF path, dropping the cyan #0ae8f0 accent from the original dual-tone mark. Restored the two-tone logo (blue + cyan) using clean, valid path data, cropped to a tight square viewBox so it fills the chip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * improvement(icons): enlarge icons, fix Zoom contrast and Quiver chip - Zoom: glyph was blue-on-blue (#0B5CFF on #2D8CFF chip); switched to currentColor so it renders as a white glyph on the blue chip. - Quiver: chip bgColor #000000 -> #FFFFFF to match the icon's near-white box, and enlarged the mark slightly (viewBox crop). - Enlarged (tightened viewBox, verified no clipping): RevenueCat, Prospeo, Granola, Firecrawl, Enrich.so, and the AWS icons (RDS, DynamoDB, SQS, CloudFormation, Athena, CloudWatch, SES, Bedrock, S3). - ZoomInfo left unchanged: it is a full red rounded-square logo that already fills its frame, so a crop would clip it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(icons): use Bright Data wordmark on white chip; repair Circleback - Bright Data: replaced the flame glyph with the official two-tone 'bright data' wordmark (provided asset), centered in a symmetric viewBox. Reverted the chip bgColor from #3d7ffc to #FFFFFF since the blue wordmark is invisible on a blue chip (the wordmark is designed for a light background). - Circleback: a minifier had rounded the pattern's image scale to scale(0), collapsing the embedded logo to zero size (invisible). Restored the correct scale (1/280 = 0.00357142857) so the C. mark renders. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(docs): sync Quiver block color card to white chip Reflects the Quiver bgColor change (#000000 -> #FFFFFF) in the docs block info card. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * improvement(icons): enlarge AWS/Cloudflare/Dagster icons, fully white Zoom - Enlarged (tighter viewBox, render-verified, no clipping): Cloudflare, Dagster, and the red AWS icons AWS IAM, Identity Center, Secrets Manager, SES, STS. Identity Center was anomalously small (filled ~32% of its frame); the group is now sized consistently (~80% fill). - Zoom: the camera lens triangle was still #0B5CFF (blue-on-blue); switched it to currentColor so the whole camera renders white on the blue chip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(wiza): consolidate individual reveal into a single operation Merges the separate Start/Get Individual Reveal operations into one Individual Reveal operation in the Wiza docs and integrations data (operationCount 5 -> 4). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * improvement(icons): size remaining AWS icons to match the set (~80% fill) Bring RDS, DynamoDB, SQS, CloudFormation, Athena, CloudWatch and S3 up to the same ~80% fill as the AWS IAM/Identity Center/Secrets Manager/SES/STS group, so all AWS icons are visually consistent. Bedrock left as-is (already ~92% fill). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(icons): use Bright Data flame mark, enlarge ZoomInfo - Bright Data: the full 'bright data' wordmark was illegible at chip size. Replaced with just the flame-'i' brand mark (blue #4280f6 on the white chip), centered. - ZoomInfo: cropped the viewBox toward the white 'Zi' so it's larger; the red rounded-square background still fills the chip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * improvement(icons): enlarge CrowdStrike icon The falcon mark sat small in its chip because the icon used a wide 768x500 viewBox (letterboxed in the square chip). Switched to a square viewBox centered on the mark so it fills ~80%, consistent with the other icons. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * fix(tables): serialize schema mutations to prevent parallel column clobber (#4812) * Make workflow description nullable * fix(tables): serialize schema mutations to prevent parallel column clobber * fix(tables): load workflow outside schema lock; use DbOrTx for getTableById * fix(tables): scale idle timeout in updateColumnType to avoid aborting large type changes * fix(tables): skip stale remap types when workflowId changes concurrently * fix(tables): scale idle timeout in updateColumnConstraints for large tables * fix(wait): resume live/draft async waits and preserve cell context on chained waits (#4814) * Make workflow description nullable * fix(wait): resume live/draft async waits and preserve cell context on chained waits * improvement(knowledge): polish tag filter dropdowns * improvement(knowledge): soften filter section labels * improvement(knowledge): soften list filter labels * fix(security): harden SSO domain registration, webhook path isolation, and CSV export (#4813) * fix(security): harden KB file access, SSO domain registration, webhook path isolation, env secrets, and CSV export * fix(sso): scope domain conflict query with indexed lower(domain) filter Address PR review: avoid a full-table scan on every SSO provider registration by filtering candidate rows in SQL with lower(domain) = <normalized>, keeping the in-memory ownership check. Also tighten the normalizeSSODomain TSDoc. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: condense env route security comments Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * icons update * chore(security): tighten inline comments in CSV export and KB file authorization Condense verbose comment blocks to concise TSDoc/single-line form; no behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(security): validate internal serve origin in KB file authorization Replace the bypassable isInternalFileUrl substring check in resolveInternalKbKey with an origin allow-list (base URL, internal API base URL, TRUSTED_ORIGINS). A crafted external host whose path is /api/files/serve/<victim-key> no longer resolves to the victim key. Relative same-origin URLs are unaffected. * style(sso): use idiomatic sql lower() comparison for domain conflict query Match the repo's prevailing `sql`lower(col) = value`` idiom for the case-insensitive SSO domain conflict lookup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(security): align workspace env admin gate with hasWorkspaceAdminAccess Use the same admin check the secrets UI uses (owner, admin permission, or org-admin) so owners and org-admins are not wrongly denied their own decrypted workspace secrets, while read-only members remain restricted to names only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(sso): rely on lower(domain) match for conflict detection, drop dead in-memory recheck Address PR review: the SQL `lower(domain) = <normalized>` predicate already excludes rows that the in-memory `normalizeSSODomain(...) === domain` recheck claimed to catch, making that recheck dead/misleading code. Match on the canonical lower-cased domain and filter purely by ownership. Malformed legacy values (wildcards, schemes, ports) never match an email domain at sign-in, so excluding them is not a gap. Test DB mock now applies the lower() predicate so the casing-variant case is genuinely exercised. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(security): scope webhook deploy path conflict to active webhooks findConflictingWebhookPathOwner omitted the isActive filter that the runtime dispatcher (findAllWebhooksForPath) applies, so an inactive but non-archived webhook from another workflow (e.g. after undeploy or failure auto-disable) would permanently block any new deployment on that path even though it never receives deliveries. Align the guard with the runtime isActive + archivedAt filter; the earliest-owner runtime check remains the authoritative cross-tenant protection. Also trims verbose TSDoc on the webhook path-isolation helpers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(security): exclude archived workflows from webhook deploy path conflict findConflictingWebhookPathOwner now joins workflow and filters isNull(workflow.archivedAt), matching the runtime dispatcher (findAllWebhooksForPath). A webhook on an archived workflow can never receive deliveries at runtime, so it must not block legitimate path reuse with a 409. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(security): anchor KB file ownership to earliest document in any state A KB file's owner is now the earliest document referencing its key regardless of state (active/archived/deleted/excluded); access is granted only when that owning document is still active. Closes the residual where an attacker could plant an active document to claim a file whose original document was archived or deleted. * updated greptile icon * revert(security): drop KB file authorization changes Reverts the knowledge-base file-access work (origin-pinning / owner-pinning / origin allow-list in verifyKBFileAccess) and its test. The other hardening fixes (SSO domain registration, webhook path isolation, workspace env secrets, CSV export) are unchanged. apps/sim/app/api/files/authorization.ts is restored to its origin/staging baseline. * fix(sso): treat caller's own user-scoped provider as owned during conflict check Self-hosters often register SSO user-scoped via the CLI script (no SSO_ORGANIZATION_ID). If they later enable organizations and reconfigure the same domain org-scoped through the UI, the conflict check previously treated their own user-scoped row as another tenant's and returned a misleading 409. Recognize the caller's own user-scoped provider as owned so that migration is allowed, while still blocking another user's or another org's domain. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * revert(security): remove workspace-env admin gate Defer to a credential-based access model (separate change). Restores GET /api/workspaces/[id]/environment to main behavior and removes the test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(security): consolidate webhook path-collision check into one helper Extract findConflictingWebhookPathOwner to lib/webhooks/utils.server.ts as the single source of truth for cross-tenant path-collision detection, used by both webhook creation paths (deploy sync and the manual /api/webhooks route). This also repairs two latent issues in the manual route's previous inline check, which queried with limit(1) and only webhook.archivedAt: - limit(1) inspected one arbitrary row, so a same-workflow row could mask a foreign collision (false negative). The shared helper scans all matching rows. - It omitted isActive/workflow.archivedAt, so inactive or archived-workflow webhooks (which never receive deliveries) permanently blocked path reuse. The helper mirrors the runtime dispatcher's filter. Same-workflow webhook reuse for upsert is now a separate, explicit lookup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * fix(security): block private/reserved IPs for hosted 1Password Connect SSRF (#4818) * fix(security): block private/reserved IPs for hosted 1Password Connect SSRF * test(security): use real isPrivateOrReservedIP and cover IPv6 edge cases * improvement(integrations): validate and expand devin, cursor, and greptile (#4820) * improvement(integrations): validate and expand devin, cursor, and greptile - devin: fix missing org_id path segment on all session endpoints, add 7 session sub-resource tools (list messages/attachments, get/append/replace tags, archive, terminate), pagination, and is_archived output - cursor: add get_api_key_info, list_models, list_repositories tools - greptile: align block and docs - normalize array outputs to default [] and tighten types * refactor(cursor): simplify list_repositories v2 array normalization Collapse the redundant `?? []` + `Array.isArray` double-guard into a single Array.isArray check, per PR review feedback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(devin): scope session-tag mapping to tag ops and normalize array tag inputs - Only map sessionTags into the tools tags param for append/replace operations, preventing stale sessionTags state from clobbering create_session tags - Fall back to a wired tags value when sessionTags is empty for tag operations - Normalize tag inputs (string or wired string[]) via normalizeTags so array values from other blocks no longer throw on .split Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(cursor): restore base64 file data in legacy download_artifact metadata The legacy CursorBlock exposes only content + metadata (no v2 file output), so metadata.data was the only way legacy-block workflows could access downloaded artifact bytes. Restore the base64 data field and document it in the outputs/type instead of dropping it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(devin): coerce terminateArchive to archive flag for boolean-wired input * docs(integrations): regenerate tool docs for new devin and cursor operations --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * fix(search-replace): don't auto-navigate when content edits invalidate the active match (#4819) * fix(search-replace): don't auto-navigate when content edits invalidate the active match * fix(search-replace): clear afterReplaceIndexRef on apply failure and zero matches * fix(search-replace): remove duplicate setActiveSearchTarget(null) on close * fix(search-replace): move afterReplaceIndexRef write inside handleApply past the guard * fix(search-replace): auto-navigate when hydration resolves with no prior active match * chore(search-replace): remove inline comments * fix(search-replace): revert !activeMatchId guard that caused immediate re-navigation after deselect * improvement(enrichments): limit company-info to fields both providers return (#4817) Hunter's company dataset returns null industry/foundedYear for many large companies (verified against the live API for Microsoft, Amazon, Google), so under the first-non-empty-wins cascade those columns appeared inconsistently across rows. Limit company-info outputs to employee count and description — the fields Hunter and PDL both reliably return — so every row is consistent. employeeCount is a string so Hunter's range bucket and PDL's exact count share the column. * fix(files): don't reject external URLs containing '..' in file parse validation (#4821) * fix(files): don't reject external URLs containing '..' in file parse validation The file block's file_fetch operation rejected any external URL whose path contained '..' (e.g. Slack files-pri slugs with a literal '...') with 'Access denied: path traversal detected'. Traversal checks only apply to local paths — external http(s) URLs are fetched with SSRF protection downstream and are never resolved against the filesystem, so they now short-circuit as valid. Internal /api/files/serve/ URLs keep full traversal protection. * test(files): fix external-URL assertion to handle undefined error * test(files): assert success explicitly in external-URL traversal test * fix(files): keep traversal protection for https URLs matching internal serve paths * feat(google-sheets): add row filtering to read with numeric operators (#4822) * feat(google-sheets): add row filtering to read with numeric operators Adds client-side row filtering to the Google Sheets read (v2) operation. Filter the returned rows by a header column using text operators (contains, not_contains, exact, not_equals, starts_with, ends_with) and numeric/ordering operators (gt, gte, lt, lte). Filtering lives in a pure, unit-tested helper (filterSheetRows) and runs over the fetched read range; an optional `filter` output reports whether the column was found and how many rows matched. Also hardens the surrounding tools: - trim spreadsheetId in write/update/append URL builders (matches read) - URL-encode the v1 read default range - expose valueInputOption for the update operation in the block Backwards compatible: with no filter requested, read output is byte- identical and the `filter` field is omitted. The filterMatchType union is widened additively (4 -> 10 values). * fix(google-sheets): correct filter metadata for missing column and header-only sheets - matchedRows is now 0 (not totalRows) when the filter column is not found, so it no longer contradicts applied=false / columnFound=false - columnFound now reflects an actual header lookup for empty/header-only sheets instead of being hardcoded true - add tests covering header-only and empty sheets with present/absent columns * fix(selectors): fetch all pages for paginated dropdown list routes (#4823) * fix(selectors): fetch all pages for paginated dropdown list routes Dropdown selectors fetched only the first page of paginated provider APIs, silently hiding results past page one. Add bounded server-side draining to the list routes across Microsoft Graph, Google, Notion, Atlassian, Linear, AWS CloudWatch, and offset/token REST APIs, plus a shared client-side drain cap in the selector hook. Response shapes, stored values, and tool execution are unchanged; CloudWatch list tools still honor a caller-supplied limit. Also fixes the Word file picker that was searching for .xlsx files. * fix(selectors): harden JSM and Monday pagination draining - JSM service-desk/request-type drains advance `start` by the actual row count returned (not the fixed page size) and stop on an empty page, so a short non-final page can't skip items. - Monday boards drain now checks `response.ok` per page, surfacing a mid-drain HTTP failure instead of treating it as an empty final page and returning a partial 200. * docs(selectors): clarify JSM drain advances start by actual row count The offset-advancement fix (advance `start` by the rows returned, not the fixed page size) landed in 7b19788a8; update the TSDoc to match so it no longer reads as advancing by `limit`. * fix(selectors): drain fetchPage in direct fetchList callers Making `fetchList` optional left three direct callers (outside the useSelectorOptions hook) calling it unguarded, which broke the build's type check. Route them through a shared `loadAllSelectorOptions` helper that uses `fetchList` when present and otherwise drains `fetchPage`. This also prevents a regression: `confluence.spaces` / `knowledge.documents` now paginate via `fetchPage` only, and these callers (search/replace, value resolution) would otherwise have silently returned no options. * chore(selectors): rename MAX_PAGE_PAGES to MAX_NOTION_PAGES for readability * fix(sso): re-check domain conflict before write and reject IP-address domains (#4825) * improvement(copilot): make copilot_messages the sole transcript store, remove JSONB dual-write (#4826) Stop writing/reading the legacy copilot_chats.messages JSONB column now that reads are cut over to copilot_messages. Make appendCopilotChatMessages the primary write (throws on failure instead of swallowing), repoint peripheral readers (workspace VFS, chat cleanup, data drains, fork, superuser import) to copilot_messages, and persist the assistant turn inside finalizeAssistantTurn's transaction so it commits atomically with the stream-marker clear. The column itself is dropped in a follow-up migration after this bakes. * feat(tables): expand filter operators (not-contains, starts/ends-with, not-in, empty) (#4827) Add does-not-contain ($ncontains), starts-with ($startsWith), ends-with ($endsWith), not-in-array ($nin, previously executed server-side but unexposed in the UI), and is-empty/is-not-empty ($empty) filter operators end-to-end — SQL builder, condition types, query-builder converters/constants, the filter UI, the Table tools/block descriptions, and docs. Also fix correctness bugs in the filter builder surfaced by the wider operator set: - Same-column AND rules (e.g. age > 18 AND age < 65, or name startsWith 'A' AND name endsWith 'Z') silently overwrote each other because the AND group was keyed by column name. They now merge into one operator object, which also makes Filter -> rules -> Filter round-trip losslessly for multi-operator columns. - $nin values were not split into an array like $in, and textual-match values like "123" were numeric-coerced (breaking the ILIKE path). - A non-boolean $empty operand from the raw API silently inverted the check; it now coerces 'true'/'false' strings and otherwise returns a 400. * improvement(copilot): stop persisting tool-call result outputs in transcripts (#4829) Opening a Mothership task could take many seconds because a single persisted assistant message in copilot_messages.content can reach hundreds of MB, almost entirely inside contentBlocks[].toolCall.result.output (e.g. a get_workflow_logs or run_workflow result). The DB query is ~2ms; the cost is detoasting that payload, shipping it to the browser, and parsing it. These outputs are dead weight on the Sim side: they are never rendered (the thread shows only tool name/title/status) and never replayed to the model (the upstream copilot service owns conversation memory). So drop result.output before it is persisted, keeping result.success/error plus the tool metadata. - add stripToolResultOutput() in persisted-message.ts - apply it in messages-store toRow (covers every write path) and in loadCopilotChatMessages (existing rows render fast on read) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * feat(providers): add Together AI, Baseten, and Ollama Cloud model providers (#4830) * feat(providers): add Together AI, Baseten, and Ollama Cloud model providers * fix(providers): guard Ollama streaming fast-path with hasActiveTools Match Together/Baseten/Fireworks: when tools are supplied but all are filtered out (usageControl 'none'), take the single streaming call instead of an extra non-streaming round-trip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(providers): filter non-chat model types from Together model list * refactor(providers): dedupe Ollama Cloud upstream schema ollamaCloudUpstreamResponseSchema was byte-for-byte identical to ollamaUpstreamResponseSchema (both /api/tags endpoints return the same { models: [{ name }] } shape). Drop the duplicate and reuse the shared schema. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * fix(knowledge): calendar view sync, deduplicate popover animation classes, type-safe filter cast * cleanup(knowledge): remove TRIGGER_BORDER_CLASS duplication, inline displayLabel, drop enabledFilterParam alias --------- Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Waleed <walif6@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Theodore Li <theo@sim.ai> Co-authored-by: andresdjasso <andresdjasso@users.noreply.github.com> * feat(blocks): add BlockMeta to Quiver and Linq; fix invalid block config fields; update skills Block fixes: - Add QuiverBlockMeta (tags + 3 templates: icon generator, diagram creator, vectorizer) - Fix QuiverBlock: remove invalid tags field from BlockConfig, IntegrationType.Design → IntegrationType.AI (Design doesn't exist in the enum) - Fix GreptileBlock: remove invalid tags field from BlockConfig, IntegrationType.DeveloperTools → IntegrationType.DevOps - Fix LinqBlock: remove invalid tags field from BlockConfig (tags belong only in BlockMeta) Skills: - add-block: add dedicated BlockMeta section with structure, rules, and registration pattern; add BlockMeta checklist items - add-integration: add BlockMeta to block structure template, add rules clarifying that tags must NOT appear on BlockConfig and integrationType must be a valid enum value; update registry snippet to include blocksMeta; add checklist items * fix(integrations): fix category dropdown by defining missing LANDING_INTEGRATIONS_DATA_PATH and regenerating integrations.json The staging merge introduced landing-content.ts but forgot to define LANDING_INTEGRATIONS_DATA_PATH in generate-docs.ts, causing the script to crash before writing integrations.json. The stale JSON had integrationTypes (plural array) from an older script version, while the Integration type and workspace UI both read integrationType (singular string) — so ALL_CATEGORY_SECTIONS bucketed to undefined and the category filters never appeared in the dropdown. Fixed by adding the missing path constant and re-running the generator. integrations.json now has 192 entries with the correct integrationType field. * fix(sidebar): restore resize handle on all pages commit3109104582wrapped the resize handle in {(isCollapsed || isOnWorkflowPage) && ...} and added a useEffect that resets sidebar width to SIDEBAR_WIDTH.MIN whenever the user navigates away from a workflow page. Together these made the sidebar non-resizable on Tasks, Tables, Knowledge Base, and every other non-workflow page. Restore the staging behavior: always render the resize handle and remove the effect that forced the width reset on page transitions. * fix(sidebar): match staging onKeyDown and tabIndex on resize handle The resize handle was still conditionalizing onKeyDown and tabIndex on isCollapsed, blocking keyboard accessibility of the separator role when expanded. Staging always attaches both unconditionally. onKeyDown={isCollapsed ? handleEdgeKeyDown : undefined} → onKeyDown={handleEdgeKeyDown} tabIndex={isCollapsed ? 0 : undefined} → tabIndex={0} * feat(integrations): show connected credentials on integration detail page When navigating to /integrations/google-docs (or any integration), a Connected section now appears above the templates listing all workspace credentials tied to that provider. Each row links back to the credential detail page (/integrations/connected/${id}) for management actions. Pairs with the earlier change that routes connected items from the integrations list to the provider detail page instead of directly to the credential detail page. * fix(integrations): rename Add in chat to Add to Sim * fix(skills): rename Add button to Add to Sim * fix(platform): restore M1/M2/M3 regressions and LazyMotion on landing page M1 — Invitation guard: re-introduce usePermissionConfig().isInvitationsDisabled alongside the workspace inviteDisabledReason check. The flag now also respects NEXT_PUBLIC_DISABLE_INVITATIONS and EE permission-group disableInvitations, not just billing policy. M2 — Settings redirects: /settings/integrations and /settings/skills now server-redirect to /integrations and /skills respectively so old bookmarks and emails don't silently land on General. M3 — Starter block search exclusion: restore block.type !== 'starter' guard in the search store so users cannot add a duplicate Starter block via the command palette. LazyMotion: restore LazyMotion + domMax/domAnimation wrappers and m.* components in landing-preview-panel and landing-preview-home. The removal was accidental (the full motion bundle was left after an import cleanup), which caused the entire framer-motion feature set to load eagerly on the landing page. * fix(integrations): revert connected list to credential detail; remove settings redirects * feat(sidebar): restore workspace switcher search with updated styling Shows a search input in the workspace dropdown when the user has more than 3 workspaces (WORKSPACE_SEARCH_THRESHOLD). Keyboard navigation: ArrowDown/Up to move through results, Enter to switch, resets on close. Styled to match the current branch (border-1/surface-5 tokens, sm text, 11px Search icon) rather than the old staging styles. Highlight state is wired through chipVariants active prop so it follows the same active appearance as clicked/hovered items. * fix(sidebar): clean up workspace search — layout, memo, and effect guard * fix(sidebar): align workspace rename input selection style with workflow rename * perf(sidebar): eliminate React re-renders during sidebar drag resize Previously, every mousemove during resize called setSidebarWidth(), which both updated the --sidebar-width CSS variable (sync) and set Zustand state (async). This caused: 1. A 1-frame transition flash on mousedown — isResizing state had to round-trip through React before the is-resizing CSS class was applied, so the width transition fired for the first pixel of movement. 2. A React re-render per pixel dragged — components reading sidebarWidth from the store (avatars, usage-indicator) lagged one frame behind the container, making the + and ... buttons appear to jump ahead. New approach: - handleMouseDown adds is-resizing directly to the sidebar DOM node before any React involvement (synchronous, no frame lag). - mousemove writes only to the CSS custom property (zero React renders). - mouseup persists the final width to Zustand exactly once. - isResizing / setIsResizing state removed from the store and hook — they are no longer needed since the class is managed via direct DOM mutation. * perf(sidebar): add requestAnimationFrame throttle to resize mousemove handler * fix(sidebar): fix drag-right lag caused by WorkspaceChrome overflow-hidden transition The sidebar-container's is-resizing class correctly suppressed its own width transition, but the two wrapper divs in WorkspaceChrome both have transition-[width]/transition-transform with a 175ms ease. The outer wrapper also has overflow-hidden, so while the sidebar content was at the correct width instantly, it was visually clipped by the outer wrapper which was still animating — causing the + and ... buttons to appear to lag behind the resize line on drag-right (not on drag-left, since shrinking doesn't clip content). Fix: add sidebar-shell-outer/sidebar-shell-inner class names to both chrome wrappers, and suppress their transitions via html.sidebar-resizing rule when a drag is active. The html.sidebar-resizing class is toggled directly in the resize hook alongside is-resizing, so it takes effect synchronously on mousedown. * fix(icons): redesign Download icon to match Upload style; fix Upload/Download confusion Download icon was missing the tray/shelf line at the bottom that Upload has, making it look like a plain arrow rather than a matched pair. Updated Download to use the same viewBox, stroke weight, and three-path structure as Upload (tray + stem + arrowhead), just pointing down. Also fix 5 places where Upload (↑) was incorrectly used for download/export actions: - files.tsx: two Download action rows in toolbar and context menu - tables/table.tsx: Export CSV toolbar button - table-context-menu.tsx: Export CSV context menu item - logs.tsx: Export toolbar button - landing-preview-logs.tsx: decorative Export button Import CSV and actual upload actions correctly keep the Upload icon. * fix(icons): replace Upload with Download on all remaining export/download actions - panel.tsx: Export workflow dropdown item - context-menu.tsx: Export in sidebar workflow context menu - chat.tsx: Export chat button - output-panel.tsx: Export console CSV button - terminal.tsx: Export console CSV button - resource-content.tsx: Export table as CSV + Download file buttons * fix(icons): fix remaining Upload→Download on download actions in files and logs - action-bar.tsx: download button in files toolbar - file-row-context-menu.tsx: Download item in file context menu - file-download.tsx: both download buttons in log details file viewer * updated block skills, settings pages, modals, buttons -> chips, blocks missing metadata * updated skill modal * improvement(resource-header): refine breadcrumb truncation ux * improvement(resource): add floating overflow text tooltips * wire up credits counter * improvement(resource-header): mute path dropdown title * refactor(resource-header): share floating-tooltip engine, prune dead overlay tooltips (#4844) Clean up the breadcrumb truncation feature for reuse and correctness: - Extract useFloatingTooltip / useIsOverflowing / FloatingTooltip into a shared floating-tooltip module. BreadcrumbSegment and FloatingOverflowText now consume one implementation instead of duplicating ~150 lines of positioning, velocity, overflow-detection, and portal logic. - Replace the hardcoded terminal-label regex in ResourceHeader with a typed `terminal` flag on BreadcrumbItem (set by the document chunk/loading crumbs), decoupling the generic header from knowledge-base copy. - Clear the path-popover close timeout on unmount and reuse the shared POPOVER_ANIMATION_CLASSES constant. - Drop the redundant manual overflow-state writes (fixes a sticky fade mask). - Revert FloatingOverflowText inside Combobox `overlayContent` back to plain truncating spans across files/logs/tables/scheduled-tasks/document: the combobox overlay is pointer-events-none, so the tooltip handlers never fired there. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * refactor(emcn,resource-header): address PR #4844 review feedback - useIsOverflowing now uses a callback ref so the ResizeObserver follows the element across mount/unmount/reassignment instead of capturing it once at mount. Safe for conditionally rendered consumers of the shared hook. (greptile P2) - Move POPOVER_ANIMATION_CLASSES out of chip-date-picker implementation internals into emcn/components/popover/popover-animation.ts, exported from the @/components/emcn barrel. Consumers now import from the module boundary. (greptile P2) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * upgrade table and styling upgrade * fix schema to include integration * fix(files): align delete icon with tables view (Trash → Trash2) Co-Authored-By: waleed <waleed@simstudio.ai> * fix(mothership): preserve blockType for integration contexts in sent messages Integration mention chips were missing their provider icons in sent messages because blockType was dropped when mapping ChatContext to messageContexts. renderIntegrationTile returns null without blockType, silently hiding the icon. * fix(mothership): allow 'integration' resource type in chat resources API The VALID_RESOURCE_TYPES allowlist was missing 'integration', causing a 400 error when adding integrations to the Mothership resource tab — so they never persisted and disappeared on refresh. * fix(ui): Add "File" title next to file resource header * fix(ui): fix resource header columns being bolded * fix(resource): keep the floating tooltip from jumping on click Gate the focus-driven show behind :focus-visible so a mouse click (which focuses the trigger) no longer re-shows the tooltip anchored to the element's bottom edge. On click the tooltip now hides cleanly instead of jumping down; keyboard focus still shows it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * perf(sidebar): eliminate unnecessary re-renders in workspace switcher for non-search users - onMouseEnter: only set highlightedIndex when showSearch is true, preventing a state update + re-render on every workspace row hover for users with ≤ 3 workspaces where the search is never shown - onOpenChange: only reset workspaceSearch and highlightedIndex when showSearch is true, since both values are always already at their defaults for non-search users and setting them triggers a pointless re-render during dropdown close - data-workspace-row-idx: only set when showSearch is true since the scroll effect that reads this attribute is already gated on showSearch * feat(search): context-aware cmd-k results on the integrations page When cmd-k is opened on the integrations page, show two new result groups: connected accounts (visible even with empty input) and catalog integrations (appear once the user types). Selecting an OAuth integration deep-links to its detail page with ?connect=oauth so the connect modal auto-opens. Non-OAuth integrations navigate to the plain detail page. Both groups are gated to the integrations page only and respect the hideIntegrationsTab permission. The credentials fetch shares the same React Query cache key as the integrations page itself (no double fetch). * refactor(emcn): make the floating tooltip the one canonical Tooltip Replace the Radix-based emcn Tooltip with the cursor-following floating tooltip so every tooltip in the app uses one consistent style. Built on the shared floating-tooltip engine (relocated into emcn), not a parallel implementation. - Move the floating-tooltip engine into emcn/components/tooltip and export it from the barrel; re-point its consumers (FloatingOverflowText, resource-header) - Extend the FloatingTooltip bubble to render arbitrary children (+ role/id for a11y) so it can back general tooltips, not just overflow text - Rebuild emcn Tooltip (Root/Trigger/Content/Provider/Shortcut/Preview) on useFloatingTooltip — compound API preserved, ~350 call sites unchanged, legacy side/align props accepted and ignored (the tooltip follows the cursor). Removes @radix-ui/react-tooltip usage (package kept for a later cleanup; react-slot retained for asChild) Note: general tooltips now show instantly (no hover delay) and follow the cursor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * style(emcn): put tooltip text on the design scale (text-caption) Replace the tooltip's ad-hoc `text-xs` + `leading-[18px]` with the semantic `text-caption` (12px) font-size token so the text styling is fully on the design scale and self-documenting, matching how the rest of the system is set up. The color already used the global `--text-body` token. No visual change (still 12px with a ~18px line height). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(sidebar): add empty task state and inline task creation - Show "No tasks yet" in the Tasks section (expanded and collapsed) when the list is empty - Clicking + now creates a task via the API and navigates directly to it, rather than navigating to home - Add isCreatingTaskRef guard to prevent double-click from spawning multiple tasks - Disable + button while creation is pending - Fall back to home navigation on creation error * invite, billing, home * improvement(seats): auto purchase seats on invitations into workspace (#4857) * improvement(seats): auto purchase seats on invitations into workspace * improve sampling for seat drift reconciler * address comments * feat(knowledge): align connector UI with integrations page styling - ConnectorTypeCard now matches integration rows: brand-colored rounded-xl tile, ArrowRight, title/subtitle hierarchy - ConnectorCard icon upgraded from flat surface-4 to branded tile (white icon on brand bg, graceful fallback) - Connector header badges use chipVariants instead of custom Button classes - Add-connector search input aligned to integrations style (h-[30px], rounded-lg, border-1) * fix(icons): trim Folder SVG viewBox to remove right-side whitespace The folder path only extends to x≈14.33 in a 15-unit viewBox, leaving ~0.5 units of empty space on the right. At 12px rendered size this produces ~0.4px extra gap (visible as ~1px on retina displays) compared to solid icons like the workflow color square. Trimming the viewBox to 14.5 units makes the folder fill its chip slot evenly. * fix(user-input): restore draft text synchronously to preserve contexts on nav The SSR-safe approach (empty useState + effect restore) created a timing window where the sync effect in useContextManagement fired with message='' before the value was set, clearing any restored contexts. Folder and workflow contexts (not re-added by applyAutoMentions) were lost on every nav-back. Revert to the staging approach: initialize value synchronously from the draft store so message is already populated when effects run, matching the behavior on staging. * fix(queue): render context chips in queued messages - Remove plainMentions from queued message rows so context chips render with icons, consistent with sent messages - Fix computeMentionRanges to use '/' prefix for skill contexts (content has the slash trigger restored at submit time, not '@') * fix(mothership): remove integrations from add-resource dropdown * fix(mothership): comment out integrations from add-resource dropdown * chore(db): drop form, templates, template_creators, template_stars tables These tables backed the Forms and Templates platform features which were intentionally removed from this branch. Clean up the DB schema to match. * chore(db): add migration metadata for 0224 drop tables * block icons, sidebar, toolbar * chore: remove remaining dead code for template-profile feature - Remove 'template-profile' from SettingsSection union type - Remove 'template-profile' entry from SECTION_TITLES - Remove now-redundant template-profile guard in settings sidebar - Remove commented-out template-profile nav item * fix(multi-select): preserve anchor on range selection for tasks and folders After a shift+click range, the anchor (lastSelectedTaskId / lastSelectedFolderId) was being updated to the end of the range (toId). This caused subsequent shift+clicks to extend from the wrong point instead of the original click. Standard behavior: anchor stays at the initial click (fromId) so repeated shift+clicks always expand/contract relative to where you started. * feat(emcn): add SearchInput component and unify search bars platform-wide - Add SearchInput to emcn: 30px chip-family filled search input matching the integrations page pattern (border-1, surface-5, leading Search icon) - Migrate all 22 search bars across settings, EE pages, and integrations to SearchInput (only layout classes allowed at callsites) - Rename Sim Keys -> Sim API Keys in nav/title; page copy now says API key - Remove components/ui input, label, and verified-badge; migrate consumers to emcn equivalents or raw inputs (table cell editor, wand prompt bar) - Delete dead EE skeleton files (data-drains, data-retention) - General settings: Home Page chip moves to header left as navigation * fix(files,tables): restore new-file editor autofocus and CSV import error toasts Both were dropped on the staging line and regressed vs production (main): - Files: the new-file editor autofocus chain (files.tsx -> file-viewer -> text-editor) was stripped by the react-doctor dead-code pass in #4544, which misread the prop-drilled `autoFocus` (consumed by an imperative `editor.focus()` effect) as unused. Restored the prop through all three layers and the one-shot focus effect so creating a new file focuses the editor immediately. - Tables: CSV import failures were silently logged with no user feedback. Restored the per-file and generic `toast.error` surfacing. * feat(home): score suggested actions by workspace signals - Derive the suggestion pool from the curated block template catalog (1,343 prompts across 172 blocks) instead of 15 hardcoded entries - Fix inverted relevance: prompts for connected providers are now boosted 4x (instantly runnable) instead of excluded; unconnected discounted 0.4x - Weight by featured (3x), popular category (1.5x), and resource gaps (no tables -> boost table starters; has KBs -> dampen KB-creation prompts) - Weighted sampling without replacement, max one suggestion per block - Connect rows weighted by catalog template count; 2 for fresh workspaces, 1 once something is connected - Key the catalog map by both versioned and base block types so gmail_v2 templates resolve (gmail, github, notion, linear were silently dropped) - Replace derive-in-effect state with a useMemo keyed by a shuffle nonce - Add suggested_action_clicked / suggested_actions_shuffled / suggested_actions_toggled PostHog events * billing, teammates * improvement(credentials): credentials invites, secrets tab wiring up (#4874) * improvement(credentials): move away from invite notion * wire up secrets ui/ux * address comments * get consistent styling by removing emcninput + text area * styling consistency * remove fallback * address comment: * refactor(ui): migrate settings & workspace UI to chip design system Migrate modals to ChipModal (showDivider, hint, resizable, size, leading, ChipModalTabs), standardize Chip variants, add ChipCombobox wrapper, and apply chip inputs/dropdowns across settings, knowledge, logs, tables, inbox, EE tabs. Render ChipModalTabs as a ChipSwitch segmented control. Align /settings/secrets detail with /integrations, refresh whitelabeling, and restore file-editor autofocus and CSV import error toasts. * fix(mothership): restore integrations to useAvailableResources for @ mention Integrations were fully removed from useAvailableResources which broke the @ mention menu since user-input shares the same hook. Now integrations are always included in the hook but excluded at the AddResourceDropdown component level, keeping them out of the sidebar + menu while remaining available for @ mention autocomplete. * refactor(settings): chip design-system consistency pass across all tabs Extract a shared chip-field shell (CHIP_FIELD_SHELL/CHIP_FIELD_INPUT) mirroring Input variant='chip' and route secrets, credential detail, and integrations credential detail through it (30px height, font-medium, focus ring). Add a Discard action to the secrets header when dirty. Group BYOK providers into Models/Search & web/Enrichment sections and align its tiles to the integrations tile. Normalize list-row typography to text-[14px]/text-[12px] and icon tiles to rounded-xl + border across api-keys, copilot, custom-tools, mcp, workflow-mcp-servers, credential-sets, and access-control. Tone the secrets Details chip and per-row affordances to ghost. Fix token correctness: raw tailwind colors to design tokens (data-drains), missing chip variant on the Snowflake role input, ColorInput chip-field reuse (whitelabeling), error token and icon sizes (workflow-mcp-servers), border token (mcp), no-results sizing (secrets), row chrome (recently-deleted), hover token and Button to Chip (access-control), and deduped textarea chrome (sso). * feat(settings): unify filter dropdowns on ChipSelect (integrations style) Add a ChipSelect emcn component — a filled chip trigger + chevron opening a DropdownMenu, matching the integrations category filter — supporting single select, multi-select (checkbox rows), grouped options, and optional in-menu search. Migrate every settings/EE filter dropdown off ChipCombobox to it: audit-logs (resource-type multi + time-range), data-retention, data-drains, general, admin (grouped tool picker), inbox status filter, and the workflow MCP-server pickers. The SSO provider-id field stays an editable combobox since it accepts free-text slugs. Also fix audit findings: chip the MCP client-secret input, normalize an MCP error-text size, and drop now-dead destination-icon code in data-drains. * fix(mentions): require explicit @ for integration mentions; decorate sent messages robustly - Bare integration names in prose (Monday, Notion, Clay) are no longer auto-converted to mentions or chipped — mention treatment is strictly opt-in via a token-starting @ (fixes the scunthorpe problem) - @-prefixed mentions still canonicalize casing (@slack -> @Slack) on both the keystroke fast-path and bulk paths (paste, template, draft, STT) - Sent/queued messages now self-sufficiently decorate @IntegrationName tokens via a text scan, covering messages sent before the input pass ran or authored outside the chat input - Integration contexts missing a resolvable blockType (messages persisted before blockType was saved) are backfilled by label lookup so their mention pills render the brand icon again * refactor(settings): section the API keys page like secrets Wrap Workspace, Personal, and the allow-personal-keys toggle in SettingsSection (muted label + divider) instead of bare bold headers, matching the secrets and BYOK pages. * fix(settings): ChipSelect renders above modals + full-width form mode Raise the ChipSelect menu to --z-popover so it layers above modal surfaces (--z-modal) instead of opening behind them. Add a fullWidth prop that stretches the trigger and right-aligns the chevron for form-field use, and apply it to the workflow MCP-server pickers. * fix(emcn): ChipSelect uses the emcn flat chevron, not lucide's square one The lucide ChevronDown is square; rendering it at the chip's 9x7 footprint stretched it. Switch to the custom emcn ChevronDown (built for that wide aspect), matching the integrations filter and ChipDropdown. * fix(emcn): ChipSelect trigger hugs its content (w-fit) In a stacked form layout the trigger was stretched by align-items: stretch, leaving an empty gap to the right of the value. Add w-fit so the chip sizes to its content (a compact pill) everywhere; fullWidth form selects are unaffected. * fix(emcn): ChipSelect uses a square lucide chevron Revert to lucide's ChevronDown sized square (size-[14px]) so it renders crisp, matching the standard select chevron used by Combobox. * renamed tasks to chats * rename and file change * improvement(billing): wire up billing, org, teammates tabs + remove deprecated subscription tab (#4887) * improvement(billing): wire up billing, org, teammates tabs + remove depr subscription tab * pass exec timeout to tool routes * reuse helper * address comments * address disable comment * chore(db): remove migration 0224 to regenerate on top of staging Co-authored-by: Cursor <cursoragent@cursor.com> * fix type errors and regen migration? * chore(db): drop branch migration 0226 ahead of staging merge; will regenerate * chore(db): regenerate migration 0226 after staging merge * externalize before compaction in fallback' * fix save/discard chips to be consistent * fix(ui): remove smodal tabs in favor of chip modal tabs * fix(ui): skip auto-scrolling on mouse highlight of workspace * fix(platform): restore settings redirects, forgot-password Enter submit, and tag tooltip visibility - Re-add SETTINGS_REDIRECTS so /settings/integrations and /settings/skills deep links redirect to their top-level routes instead of rendering an empty settings panel (accidentally removed in86da193cc3one minute aftercca5054cf6added it) - Add opt-in onSubmit to ChipModalField input/email variants and wire it in the forgot-password modal so Enter submits again (lost in the ChipModal conversion) - Knowledge tag tooltip: drop the max-h/overflow-y-auto clamp that the pointer-events-none floating tooltip made unreachable; truncate each tag row instead so all tags stay visible with bounded height * chore(db): remove migration 0226_third_spot before staging merge * chore(db): regenerate migration as 0227 after staging merge * feat(telemetry): add posthog + audit coverage for new platform actions Audit log (compliance/permission-relevant only): - org_seat.provisioned — seat auto-purchased when an invite acceptance grows the org (actor = accepting user, includes seat delta) - org_plan.converted — Pro→Team conversion triggered by invite acceptance - org_seat.drift_reconciled — hourly cron healed a drifted seat count - credential_member.added/removed/role_changed — credential sharing surface was previously fully unaudited - table.created — parity with existing table.updated/deleted - skill updates now record skill.updated instead of mislabeled skill.created PostHog: - seats_provisioned, credential_shared/unshared, environment_updated/deleted (key counts only, never names/values) - table_import_started/completed — background CSV imports previously had zero failure observability - table_exported, file_downloaded, skill_updated - credential_connected now fires for OAuth completions (draft-hooks), credential_deleted for OAuth disconnects; previously only manual credentials were tracked - table_workflow_run gains deployment_mode (live/deployed/mixed) Also: logger.warn on all new credential-admin 403 denials (members + environment routes); invite-created audit enriched with enforcedFixedSeats/plan. Deliberately excluded as noise: per-hour dead-letter audit rows (would re-record the same stuck event every cron run) and a duplicate system-actor org_plan.converted in the Stripe outbox handler. * feat(home): fill textarea on suggested prompt click instead of sending Clicking a prompt action in the Suggested Actions panel now populates the Mothership user-input textarea (via applyAutoMentions) and focuses it with the caret at end, rather than immediately submitting. The user can review, edit, and send manually. * fix(templates): name owning integration in featured block template prompts Four featured prompts omitted their owning integration name, making them unbranded and disconnected from their title. Each prompt now explicitly names GitHub or Google Sheets so mentionifyIntegrations renders the chip and the copy reads as a self-contained agent-building instruction. * fix(templates): rewrite fragment prompts so each names its integration and reads as a complete instruction Featured and non-featured block template prompts that either omitted the owning integration's canonical name or were phrased as marketing fragments rather than natural user instructions have been rewritten. Each updated prompt now starts with an imperative verb ("Build a workflow that…"), names the owning integration explicitly so the @-mention chip renders correctly, and aligns with the entry's title. Files changed: salesforce, hubspot (×2), github, slack, airtable, firecrawl, iam. * fix(templates): name integration in remaining block template prompts - google_docs.ts: replace 'Google Doc' with 'Google Docs document' in 4 prompts; update title 'Meeting notes to Google Doc' to 'Meeting notes to Google Docs' - google_sheets.ts: replace 'Google Sheet' with 'Google Sheets spreadsheet/Google Sheets' in 3 non-featured prompts - slack.ts: replace 'Google Doc' with 'Google Docs document' in 'Daily standup summary' - stripe.ts: replace 'Google Sheet'/'Slacks' with 'Google Sheets'/'Slack' in 'Weekly metrics report' - reddit.ts: add 'Reddit' to 3 prompts that only referenced subreddits - notion.ts: rewrite featured prompt to start with a verb and name Notion - jira.ts: rewrite featured marketing-fragment prompt to start with a verb - linear.ts: rewrite featured marketing-fragment prompt to start with a verb - gmail.ts: rewrite featured marketing-fragment prompt to start with a verb and name Gmail * fix(icons): convert monochrome dark brand icons to currentColor for dark mode LinkupIcon, InfisicalIcon, IntercomIcon, LumaIcon, GranolaIcon, OnePasswordIcon, and RailwayIcon were hardcoded to black/near-black fills/strokes, making them invisible in bare DARK mode. Convert all to currentColor so they follow the theme-aware text color. Add iconColor: '#286efa' to IntercomBlock (Intercom brand blue is a confident mid-tone, safe on both themes). Two-tone icons (StagehandIcon, AgentPhoneIcon, QuiverIcon) are left unchanged because their white details are structural — converting to single-tone would destroy logo legibility. * removed color, user input, sidebar, suggested actions, chips/emcn * removed color migration * improve(blocks): audit block catalog metadata for accuracy and fill gaps - Fix template prompts that claimed capabilities blocks don't have (fabricated triggers, non-existent tools) across ~98 integrations - Normalize integration tags to family conventions and valid union values - Align alsoIntegrations and modules with template prompt content - Add BlockMeta for circleback, imap, and rss trigger integrations - Add templates to clickhouse and greptile metas - Remove duplicate PageSpeed deploy-gate template * chore(telemetry): drop org_seat.drift_reconciled audit System self-heal bookkeeping doesn't belong in the user-facing audit trail — membership and seat-purchase changes are already audited, and the cron's logger output covers ops visibility. * chore(db): consolidate branch migrations into single 0227 * fix(emcn): make ChipModal scroll internally when content exceeds viewport The Modal→ChipModal migration dropped the old ModalBody scroll container: ChipModal renders ModalContent bare (no overflow-hidden) and its wrappers had no min-h-0 chain, so tall modals (e.g. New data drain with an S3 destination) overflowed max-h-[84vh] off-screen with no way to scroll. Complete the flex min-h-0 chain through ChipModal's frame and give ChipModalBody flex-1 min-h-0 overflow-y-auto — header/footer stay pinned, body scrolls only when constrained. Short modals are unaffected (max-h caps, it doesn't stretch), and dropdowns inside the body are Radix-portaled so the new scroll container cannot clip them. Five modals that had locally patched this with max-h-[Nvh] overrides keep working unchanged. * fix(emcn): don't close modal when dismissing a dropdown via outside click Radix dispatches pointer-down-outside to every open dismissable layer at once, so clicking outside an open dropdown/select inside a modal closed both the dropdown and the modal in one jarring step. ModalContent now prevents its own dismissal while a portaled popper layer is open — the first outside click closes just the popper, the next one closes the modal. * docs(skills): de-duplicate and correct agent docs; canonical styling tokens; mirror sim-sandbox rule * docs(skills): broaden boundary-raw-fetch scope note, sync cursor rule mirrors * fix(emcn): harden modal popper guard and exempt caret-anchored dropdowns from body scroll Adversarial review follow-ups to the ChipModal scroll + dismissal fixes: - Popper guard now requires data-state="open" inside the popper wrapper, so a dropdown that is merely animating closed no longer swallows the next outside click on the modal (DropdownMenu has an exit animation that keeps its wrapper mounted briefly) - Port the same guard to SModalContent for consistency - custom-tool-modal: opt its body out of the chrome scroll container (flex-none + overflow-visible); the caret-anchored EnvVar/Tag autocomplete dropdowns are absolute-positioned inside the body and must spill past its bounds rather than clip against a scroll boundary * refactor(billing): drop unnecessary useCallback wrappers from event handlers * fix(data-drains): complete chip migration of destination forms and fill missing placeholders Finishes the in-flight FormField → ChipModalField conversion for all destination form specs and adds the placeholders that several inputs never had (S3 bucket/region/access keys, Azure account key, Datadog API key, webhook signing secret/bearer token) — the cause of the New Data Drain modal showing placeholder-less inputs inconsistently. * fix(emcn): broaden modal popper guard to onInteractOutside Covers the focusOutside dismissal path too: when a popper's focus scope unwinds on close, the transient focus shift could still dismiss the modal (and simultaneous body pointer-events lock teardown could freeze the page). Same data-state="open" scoping as the pointer guard. * fix(emcn): harden modal outside interactions * update audit mock --------- Co-authored-by: andres <k62hc5kjst@privaterelay.appleid.com> Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Waleed <walif6@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Theodore Li <theo@sim.ai> Co-authored-by: andresdjasso <andresdjasso@users.noreply.github.com> Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai> Co-authored-by: waleed <waleed@simstudio.ai>
This commit is contained in:
co-authored by
andres
Vikhyath Mondreti
Cursor
Waleed
Claude Opus 4.8
Theodore Li
andresdjasso
Vikhyath Mondreti
waleed
parent
800f56f7b9
commit
0075ab9cf6
@@ -627,19 +627,78 @@ export const ServiceV2Block: BlockConfig = {
|
||||
}
|
||||
```
|
||||
|
||||
## Block Metadata (BlockMeta)
|
||||
|
||||
Every integration block **must** export a `{Service}BlockMeta` object at the bottom of the block file. This metadata drives the integration catalog, tag filters, and workflow template suggestions shown to users.
|
||||
|
||||
### Structure
|
||||
|
||||
```typescript
|
||||
import type { BlockConfig, BlockMeta } from '@/blocks/types'
|
||||
|
||||
// ... block definition above ...
|
||||
|
||||
export const {Service}BlockMeta = {
|
||||
tags: ['messaging', 'automation'], // Same tags as the block's tags field
|
||||
templates: [ // Optional but strongly encouraged
|
||||
{
|
||||
icon: {Service}Icon,
|
||||
title: '{Service} use-case title',
|
||||
prompt: 'Build a workflow that ...',
|
||||
modules: ['agent', 'workflows'], // Modules the template uses
|
||||
category: 'productivity', // Template category
|
||||
tags: ['automation'], // Template-level tags
|
||||
alsoIntegrations: ['slack'], // Other blocks referenced in the prompt (optional)
|
||||
},
|
||||
],
|
||||
} as const satisfies BlockMeta
|
||||
```
|
||||
|
||||
### Rules
|
||||
|
||||
- **Import `BlockMeta`** from `@/blocks/types` alongside `BlockConfig`
|
||||
- **`tags`** must match the `tags` array on the block config exactly
|
||||
- **Templates are optional** but should be added for any integration that has a recognizable use case — aim for 2–4 templates per block
|
||||
- **Template `prompt`** should start with "Build a workflow that..." or "Create a workflow that..." and be concrete enough to generate a real workflow in Mothership
|
||||
- **Template `modules`** lists the Sim modules the template relies on: `'knowledge-base' | 'tables' | 'files' | 'workflows' | 'scheduled' | 'agent'`
|
||||
- **Template `category`** is one of: `'popular' | 'sales' | 'support' | 'engineering' | 'marketing' | 'productivity' | 'operations'`
|
||||
- **`alsoIntegrations`** names other block types (e.g. `'slack'`, `'linear'`) referenced in the template prompt — helps the catalog surface this template when those blocks are selected
|
||||
- Place the export **after** the main `{Service}Block` export, at the very bottom of the file
|
||||
|
||||
### Register in the blocksMeta object
|
||||
|
||||
After adding `{Service}BlockMeta` to the block file, register it in `apps/sim/blocks/registry.ts`:
|
||||
|
||||
```typescript
|
||||
// Add import (alongside the block import, alphabetically)
|
||||
import { ServiceBlock, ServiceBlockMeta } from '@/blocks/blocks/service'
|
||||
|
||||
// Add to blocksMeta object (alphabetically)
|
||||
export const blocksMeta = {
|
||||
// ... existing entries ...
|
||||
service: ServiceBlockMeta,
|
||||
}
|
||||
```
|
||||
|
||||
## Registering Blocks
|
||||
|
||||
After creating the block, remind the user to:
|
||||
1. Import in `apps/sim/blocks/registry.ts`
|
||||
1. Import `{Service}Block` and `{Service}BlockMeta` in `apps/sim/blocks/registry.ts`
|
||||
2. Add to the `registry` object (alphabetically):
|
||||
3. Add to the `blocksMeta` object (alphabetically):
|
||||
|
||||
```typescript
|
||||
import { ServiceBlock } from '@/blocks/blocks/service'
|
||||
import { ServiceBlock, ServiceBlockMeta } from '@/blocks/blocks/service'
|
||||
|
||||
export const registry: Record<string, BlockConfig> = {
|
||||
// ... existing blocks ...
|
||||
service: ServiceBlock,
|
||||
}
|
||||
|
||||
export const blocksMeta = {
|
||||
// ... existing entries ...
|
||||
service: ServiceBlockMeta,
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
@@ -827,7 +886,10 @@ All tool IDs referenced in `tools.access` and returned by `tools.config.tool` MU
|
||||
- [ ] Tools.access lists all tool IDs (snake_case)
|
||||
- [ ] Tools.config.tool returns correct tool ID (snake_case)
|
||||
- [ ] Outputs match tool outputs
|
||||
- [ ] Block registered in registry.ts
|
||||
- [ ] Block registered in `registry.ts` blocks object (alphabetically)
|
||||
- [ ] `{Service}BlockMeta` exported at bottom of block file with `tags` and `templates`
|
||||
- [ ] `BlockMeta` imported from `@/blocks/types` alongside `BlockConfig`
|
||||
- [ ] Block meta registered in `registry.ts` blocksMeta object (alphabetically)
|
||||
- [ ] If icon missing: asked user to provide SVG
|
||||
- [ ] If triggers exist: `triggers` config set, trigger subBlocks spread
|
||||
- [ ] Optional/rarely-used fields set to `mode: 'advanced'`
|
||||
|
||||
@@ -128,7 +128,7 @@ export const {service}{Action}Tool: ToolConfig<Params, Response> = {
|
||||
### Block Structure
|
||||
```typescript
|
||||
import { {Service}Icon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import type { BlockConfig, BlockMeta } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
|
||||
@@ -177,8 +177,32 @@ export const {Service}Block: BlockConfig = {
|
||||
|
||||
outputs: { /* ... */ },
|
||||
}
|
||||
|
||||
export const {Service}BlockMeta = {
|
||||
tags: ['tag1', 'tag2'], // IntegrationTag values matching the service's capabilities
|
||||
templates: [
|
||||
{
|
||||
icon: {Service}Icon,
|
||||
title: '{Service} use-case title',
|
||||
prompt: 'Build a workflow that ...',
|
||||
modules: ['agent', 'workflows'],
|
||||
category: 'productivity',
|
||||
tags: ['automation'],
|
||||
alsoIntegrations: ['slack'], // Optional: other blocks referenced in the prompt
|
||||
},
|
||||
],
|
||||
} as const satisfies BlockMeta
|
||||
```
|
||||
|
||||
### BlockMeta rules
|
||||
|
||||
- **Tags**: Use `IntegrationTag` values from `@/blocks/types`. Do NOT add a `tags` field to the `BlockConfig` object — tags belong only in `BlockMeta`.
|
||||
- **`integrationType`**: Must be a valid `IntegrationType` enum value (AI, Analytics, Commerce, Communication, Databases, DevOps, Documents, Email, HR, Marketing, Observability, Productivity, Sales, Search, Security, Support). Never invent a value that doesn't exist in the enum.
|
||||
- **Templates**: Aim for 2–4 templates per integration. Prompts should be concrete enough to generate a real workflow in Mothership. Start with "Build a workflow that..." or "Create a workflow that...".
|
||||
- **`alsoIntegrations`**: List other block type IDs referenced in the template prompt (e.g. `'slack'`, `'linear'`).
|
||||
- Place `{Service}BlockMeta` at the very bottom of the file, after the main block export.
|
||||
- Register it in both the import and the `blocksMeta` object in `registry.ts`.
|
||||
|
||||
### Key SubBlock Patterns
|
||||
|
||||
**Condition-based visibility:**
|
||||
@@ -359,14 +383,20 @@ export const tools: Record<string, ToolConfig> = {
|
||||
### Block Registry (`apps/sim/blocks/registry.ts`)
|
||||
|
||||
```typescript
|
||||
// Add import (alphabetically)
|
||||
import { {Service}Block } from '@/blocks/blocks/{service}'
|
||||
// Add import (alphabetically) — include BlockMeta
|
||||
import { {Service}Block, {Service}BlockMeta } from '@/blocks/blocks/{service}'
|
||||
|
||||
// Add to registry (alphabetically)
|
||||
// Add to blocks registry (alphabetically)
|
||||
export const registry: Record<string, BlockConfig> = {
|
||||
// ... existing blocks ...
|
||||
{service}: {Service}Block,
|
||||
}
|
||||
|
||||
// Add to blocksMeta registry (alphabetically)
|
||||
export const blocksMeta = {
|
||||
// ... existing entries ...
|
||||
{service}: {Service}BlockMeta,
|
||||
}
|
||||
```
|
||||
|
||||
### Trigger Registry (`apps/sim/triggers/registry.ts`) - If triggers exist
|
||||
@@ -433,7 +463,12 @@ If creating V2 versions (API-aligned outputs):
|
||||
- [ ] Configured tools.access with all tool IDs
|
||||
- [ ] Configured tools.config.tool selector
|
||||
- [ ] Defined outputs matching tool outputs
|
||||
- [ ] Registered block in `blocks/registry.ts`
|
||||
- [ ] `integrationType` uses a valid `IntegrationType` enum value (no invented values)
|
||||
- [ ] No `tags` field on the `BlockConfig` object (tags live only in `BlockMeta`)
|
||||
- [ ] `{Service}BlockMeta` exported at bottom of file with `tags` and `templates`
|
||||
- [ ] `BlockMeta` type imported from `@/blocks/types` alongside `BlockConfig`
|
||||
- [ ] Block registered in `blocks/registry.ts` blocks object (alphabetically)
|
||||
- [ ] Block meta registered in `blocks/registry.ts` blocksMeta object (alphabetically)
|
||||
- [ ] If triggers: set `triggers.enabled` and `triggers.available`
|
||||
- [ ] If triggers: spread trigger subBlocks with `getTrigger()`
|
||||
|
||||
|
||||
@@ -429,7 +429,7 @@ Maps multiple UI fields to a single serialized parameter:
|
||||
|
||||
**Critical constraints:**
|
||||
- `canonicalParamId` must NOT match any other subblock's `id` in the same block (causes conflicts)
|
||||
- `canonicalParamId` must be unique per block (only one basic/advanced pair per canonicalParamId)
|
||||
- A `canonicalParamId` links exactly one basic/advanced pair for a single logical parameter. Do NOT reuse the same `canonicalParamId` for different parameters, even under mutually-exclusive conditions/operations
|
||||
- ONLY use `canonicalParamId` to link basic/advanced mode alternatives for the same logical parameter
|
||||
- Do NOT use it for any other purpose
|
||||
|
||||
@@ -519,7 +519,10 @@ tools: {
|
||||
Block outputs only support:
|
||||
- `type` - The data type ('string', 'number', 'boolean', 'json', 'array')
|
||||
- `description` - Human readable description
|
||||
- Nested object structure (for complex types)
|
||||
- `condition` - Optional visibility condition
|
||||
- `hiddenFromDisplay` - Optional flag to hide from the output display
|
||||
|
||||
**Nested object/`properties` outputs are tool-output-only and will fail TypeScript at build time on block outputs.** For complex shapes use `type: 'json'` and describe the inner fields in the `description` string.
|
||||
|
||||
```typescript
|
||||
outputs: {
|
||||
@@ -542,7 +545,7 @@ outputs: {
|
||||
|
||||
### Typed JSON Outputs
|
||||
|
||||
When using `type: 'json'` and you know the object shape in advance, **describe the inner fields in the description** so downstream blocks know what properties are available. For well-known, stable objects, use nested output definitions instead:
|
||||
When using `type: 'json'` and you know the object shape in advance, **describe the inner fields in the description** so downstream blocks know what properties are available. Block outputs have no nested `properties` form — always keep the output flat and put the shape in the `description`:
|
||||
|
||||
```typescript
|
||||
outputs: {
|
||||
@@ -554,26 +557,10 @@ outputs: {
|
||||
type: 'json',
|
||||
description: 'Zone plan information (id, name, price, currency, frequency, is_subscribed)',
|
||||
},
|
||||
|
||||
// BEST: Use nested output definition when the shape is stable and well-known
|
||||
plan: {
|
||||
id: { type: 'string', description: 'Plan identifier' },
|
||||
name: { type: 'string', description: 'Plan name' },
|
||||
price: { type: 'number', description: 'Plan price' },
|
||||
currency: { type: 'string', description: 'Price currency' },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Use the nested pattern when:
|
||||
- The object has a small, stable set of fields (< 10)
|
||||
- Downstream blocks will commonly access specific properties
|
||||
- The API response shape is well-documented and unlikely to change
|
||||
|
||||
Use `type: 'json'` with a descriptive string when:
|
||||
- The object has many fields or a dynamic shape
|
||||
- It represents a list/array of items
|
||||
- The shape varies by operation
|
||||
Nested object outputs (`plan: { id: { type: 'string' }, ... }`) are a **tool-output** feature only — `OutputFieldDefinition` for blocks does not allow them and they fail TypeScript at build time.
|
||||
|
||||
## V2 Block Pattern
|
||||
|
||||
@@ -798,6 +785,33 @@ Use `wandConfig` for fields that are hard to fill out manually, such as timestam
|
||||
|
||||
All tool IDs referenced in `tools.access` and returned by `tools.config.tool` MUST use `snake_case` (e.g., `x_create_tweet`, `slack_send_message`). Never use camelCase or PascalCase.
|
||||
|
||||
## BlockMeta (Required)
|
||||
|
||||
Every block file must export a `{Service}BlockMeta` alongside the block — **minimum 7 templates**. Look at existing examples in `apps/sim/blocks/blocks/` (e.g. `browser_use.ts`, `google_sheets.ts`) for the pattern.
|
||||
|
||||
```typescript
|
||||
import type { BlockMeta } from '@/blocks/types'
|
||||
|
||||
export const {Service}BlockMeta = {
|
||||
tags: ['tag1', 'tag2'], // IntegrationTag[]
|
||||
templates: [
|
||||
{
|
||||
icon: {Service}Icon,
|
||||
title: '{Service} <use-case>', // 2–5 words
|
||||
prompt: 'Build a workflow that...', // specific use case, 1–3 sentences
|
||||
modules: ['agent', 'workflows'], // 'agent' | 'workflows' | 'tables' | 'files' | 'scheduled' | 'knowledge-base'
|
||||
category: 'operations', // 'operations' | 'marketing' | 'sales' | 'engineering' | 'productivity' | 'support' | 'popular'
|
||||
tags: ['automation'],
|
||||
alsoIntegrations: ['slack'], // optional — other block IDs referenced in the prompt
|
||||
featured: true, // optional
|
||||
},
|
||||
// ... at least 6 more
|
||||
],
|
||||
} as const satisfies BlockMeta
|
||||
```
|
||||
|
||||
Derive templates from the service's real use cases. Each prompt should name a concrete trigger, transformation, and output — not a generic description of what the service does.
|
||||
|
||||
## Checklist Before Finishing
|
||||
|
||||
- [ ] `integrationType` is set to the correct `IntegrationType` enum value
|
||||
@@ -816,6 +830,7 @@ All tool IDs referenced in `tools.access` and returned by `tools.config.tool` MU
|
||||
- [ ] If triggers exist: `triggers` config set, trigger subBlocks spread
|
||||
- [ ] Optional/rarely-used fields set to `mode: 'advanced'`
|
||||
- [ ] Timestamps and complex inputs have `wandConfig` enabled
|
||||
- [ ] Exported `{Service}BlockMeta` with at least 7 templates
|
||||
|
||||
## Final Validation (Required)
|
||||
|
||||
@@ -829,3 +844,4 @@ After creating the block, you MUST validate it against every tool it references:
|
||||
- Type coercions in `tools.config.params` for any params that need conversion (Number(), Boolean(), JSON.parse())
|
||||
3. **Verify block outputs** cover the key fields returned by all tools
|
||||
4. **Verify conditions** — each subBlock should only show for the operations that actually use it
|
||||
5. **Verify `{Service}BlockMeta` is exported** with at least 7 templates, each having `icon`, `title`, `prompt`, `modules`, `category`, and `tags`
|
||||
|
||||
@@ -67,7 +67,7 @@ export const {service}Connector: ConnectorConfig = {
|
||||
|
||||
configFields: [
|
||||
// Rendered dynamically by the add-connector modal UI
|
||||
// Supports 'short-input' and 'dropdown' types
|
||||
// Supports 'short-input', 'dropdown', and 'selector' types — see ConfigField Types below
|
||||
],
|
||||
|
||||
listDocuments: async (accessToken, sourceConfig, cursor) => {
|
||||
@@ -408,6 +408,16 @@ Each entry has:
|
||||
Users can opt out of specific tags in the modal. Disabled IDs are stored in `sourceConfig.disabledTagIds`.
|
||||
The assigned mapping (`semantic id → slot`) is stored in `sourceConfig.tagSlotMapping`.
|
||||
|
||||
## `@/connectors/utils` Helpers
|
||||
|
||||
Reuse these instead of inlining the same logic (the validator enforces them):
|
||||
|
||||
- `htmlToPlainText(html)` — strip HTML to plain text before indexing `ExternalDocument.content`. Never index raw HTML.
|
||||
- `computeContentHash(content)` — stable content hash for change detection.
|
||||
- `parseTagDate(value)` — parse to a valid `Date` or `undefined` (guards Invalid Date). Use in `mapTags` for date fields.
|
||||
- `joinTagArray(value)` — validate an array and join to a comma-separated string, or `undefined`. Use in `mapTags` for array/label fields.
|
||||
- `parseMultiValue(value)` — normalize a value into a `string[]`.
|
||||
|
||||
## mapTags — Metadata to Semantic Keys
|
||||
|
||||
Maps source metadata to semantic tag keys. Required if `tagDefinitions` is set.
|
||||
@@ -416,13 +426,17 @@ using the `tagSlotMapping` stored on the connector.
|
||||
|
||||
Return keys must match the `id` values declared in `tagDefinitions`.
|
||||
|
||||
Use the `@/connectors/utils` helpers for the common transforms — don't hand-roll date/array validation:
|
||||
|
||||
```typescript
|
||||
import { joinTagArray, parseTagDate } from '@/connectors/utils'
|
||||
|
||||
mapTags: (metadata: Record<string, unknown>): Record<string, unknown> => {
|
||||
const result: Record<string, unknown> = {}
|
||||
|
||||
// Validate arrays before casting — metadata may be malformed
|
||||
const labels = Array.isArray(metadata.labels) ? (metadata.labels as string[]) : []
|
||||
if (labels.length > 0) result.labels = labels.join(', ')
|
||||
// joinTagArray validates the array and joins to a comma-separated string (undefined if empty)
|
||||
const labels = joinTagArray(metadata.labels)
|
||||
if (labels) result.labels = labels
|
||||
|
||||
// Validate numbers — guard against NaN
|
||||
if (metadata.version != null) {
|
||||
@@ -430,11 +444,9 @@ mapTags: (metadata: Record<string, unknown>): Record<string, unknown> => {
|
||||
if (!Number.isNaN(num)) result.version = num
|
||||
}
|
||||
|
||||
// Validate dates — guard against Invalid Date
|
||||
if (typeof metadata.lastModified === 'string') {
|
||||
const date = new Date(metadata.lastModified)
|
||||
if (!Number.isNaN(date.getTime())) result.lastModified = date
|
||||
}
|
||||
// parseTagDate returns a valid Date or undefined (guards against Invalid Date)
|
||||
const lastModified = parseTagDate(metadata.lastModified)
|
||||
if (lastModified) result.lastModified = lastModified
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ When a tool has hosted key support, Sim provides its own API key if the user has
|
||||
|
||||
| Step | What | Where |
|
||||
|------|------|-------|
|
||||
| 1 | Register BYOK provider ID | `tools/types.ts`, `app/api/workspaces/[id]/byok-keys/route.ts` |
|
||||
| 1 | Register BYOK provider ID | `tools/types.ts`, `lib/api/contracts/byok-keys.ts` |
|
||||
| 2 | Research the API's pricing and rate limits | API docs / pricing page (before writing any code) |
|
||||
| 3 | Add `hosting` config to the tool | `tools/{service}/{action}.ts` |
|
||||
| 4 | Hide API key field when hosted | `blocks/blocks/{service}.ts` |
|
||||
@@ -30,10 +30,15 @@ export type BYOKProviderId =
|
||||
| 'your_service'
|
||||
```
|
||||
|
||||
Then add it to `VALID_PROVIDERS` in `app/api/workspaces/[id]/byok-keys/route.ts`:
|
||||
Then add the same provider id to the `byokProviderIdSchema` enum in `lib/api/contracts/byok-keys.ts` (this is what the byok-keys route validates against):
|
||||
|
||||
```typescript
|
||||
const VALID_PROVIDERS = ['openai', 'anthropic', 'google', 'mistral', 'your_service'] as const
|
||||
export const byokProviderIdSchema = z.enum([
|
||||
'openai',
|
||||
'anthropic',
|
||||
// ...existing providers
|
||||
'your_service',
|
||||
])
|
||||
```
|
||||
|
||||
## Step 2: Research the API's Pricing Model and Rate Limits
|
||||
@@ -230,7 +235,7 @@ Both subblocks share the same `id: 'apiKey'`, so the same value flows to the too
|
||||
To exclude multiple operations, use an array: `{ field: 'operation', value: ['op_a', 'op_b'] }`.
|
||||
|
||||
**Reference implementations:**
|
||||
- **Exa** (`blocks/blocks/exa.ts`): `research` operation excluded from hosting — lines 309-329
|
||||
- **Exa** (`blocks/blocks/exa.ts`): `exa_research` operation excluded from hosting — duplicate `apiKey` pair around lines ~348-365
|
||||
- **Google Maps** (`blocks/blocks/google_maps.ts`): `speed_limits` operation excluded from hosting (deprecated Roads API)
|
||||
|
||||
## Step 5: Add to the BYOK Settings UI
|
||||
@@ -284,7 +289,7 @@ This summary helps reviewers verify that the pricing and rate limiting are well-
|
||||
## Checklist
|
||||
|
||||
- [ ] Provider added to `BYOKProviderId` in `tools/types.ts`
|
||||
- [ ] Provider added to `VALID_PROVIDERS` in the BYOK keys API route
|
||||
- [ ] Provider added to `byokProviderIdSchema` enum in `lib/api/contracts/byok-keys.ts`
|
||||
- [ ] API pricing docs researched — understand per-unit cost and whether the API reports cost in responses
|
||||
- [ ] API rate limits researched — understand RPM/TPM limits, per-key vs per-account, and plan tiers
|
||||
- [ ] `hosting` config added to the tool with `envKeyPrefix`, `apiKeyParam`, `byokProviderId`, `pricing`, and `rateLimit`
|
||||
|
||||
@@ -21,7 +21,7 @@ Adding an integration involves these steps in order:
|
||||
## Step 1: Research the API
|
||||
|
||||
Before writing any code:
|
||||
1. Use Context7 to find official documentation: `mcp__plugin_context7_context7__resolve-library-id`
|
||||
1. Use Context7 to find official documentation: `mcp__context7__resolve-library-id`, then fetch with `mcp__context7__query-docs`
|
||||
2. Or use WebFetch to read API docs directly
|
||||
3. Identify:
|
||||
- Authentication method (OAuth, API Key, both)
|
||||
@@ -221,6 +221,28 @@ export const {Service}Block: BlockConfig = {
|
||||
- **Inputs section:** Must list canonical param IDs (e.g., `fileId`), NOT raw subblock IDs (e.g., `fileSelector`, `manualFileId`)
|
||||
- **Params function:** Must use canonical param IDs, NOT raw subblock IDs (raw IDs are deleted after canonical transformation)
|
||||
|
||||
### BlockMeta (Required)
|
||||
|
||||
Export a `{Service}BlockMeta` in the same file as the block — **minimum 7 templates**. See `add-block.md` → "BlockMeta (Required)" for valid `modules` and `category` values and the full pattern.
|
||||
|
||||
```typescript
|
||||
export const {Service}BlockMeta = {
|
||||
tags: ['tag1', 'tag2'],
|
||||
templates: [
|
||||
{
|
||||
icon: {Service}Icon,
|
||||
title: '{Service} <use-case>',
|
||||
prompt: 'Build a workflow that...', // concrete trigger → transformation → output
|
||||
modules: ['agent', 'workflows'],
|
||||
category: 'operations',
|
||||
tags: ['automation'],
|
||||
alsoIntegrations: ['slack'], // when the prompt references another service
|
||||
},
|
||||
// ... at least 6 more
|
||||
],
|
||||
} as const satisfies BlockMeta
|
||||
```
|
||||
|
||||
## Step 4: Add Icon
|
||||
|
||||
### File Location
|
||||
@@ -376,9 +398,9 @@ export const TRIGGER_REGISTRY: TriggerRegistry = {
|
||||
|
||||
## Step 7: Generate Docs
|
||||
|
||||
Run the documentation generator:
|
||||
Run the documentation generator (from `apps/sim`):
|
||||
```bash
|
||||
bun run scripts/generate-docs.ts
|
||||
bun run generate-docs
|
||||
```
|
||||
|
||||
This creates `apps/docs/content/docs/en/tools/{service}.mdx`
|
||||
@@ -424,6 +446,7 @@ If creating V2 versions (API-aligned outputs):
|
||||
- [ ] Registered block in `blocks/registry.ts`
|
||||
- [ ] If triggers: set `triggers.enabled` and `triggers.available`
|
||||
- [ ] If triggers: spread trigger subBlocks with `getTrigger()`
|
||||
- [ ] Exported `{Service}BlockMeta` with at least 7 templates
|
||||
|
||||
### OAuth Scopes (if OAuth service)
|
||||
- [ ] Defined scopes in `lib/oauth/oauth.ts` under `OAUTH_PROVIDERS`
|
||||
@@ -454,6 +477,7 @@ If creating V2 versions (API-aligned outputs):
|
||||
- [ ] Verified block subBlocks cover all required tool params with correct conditions
|
||||
- [ ] Verified block outputs match what the tools actually return
|
||||
- [ ] Verified `tools.config.params` correctly maps and coerces all param types
|
||||
- [ ] `{Service}BlockMeta` exported with at least 7 templates, each having `icon`, `title`, `prompt`, `modules`, `category`, and `tags`
|
||||
|
||||
## Example Command
|
||||
|
||||
@@ -569,33 +593,29 @@ Create `apps/sim/app/api/tools/{service}/{action}/route.ts`:
|
||||
```typescript
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { {service}UploadContract } from '@/lib/api/contracts/tools/{service}'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { FileInputSchema, type RawFileInput } from '@/lib/uploads/utils/file-schemas'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { type RawFileInput } from '@/lib/uploads/utils/file-schemas'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
|
||||
const logger = createLogger('{Service}UploadAPI')
|
||||
|
||||
const RequestSchema = z.object({
|
||||
accessToken: z.string(),
|
||||
file: FileInputSchema.optional().nullable(),
|
||||
// Legacy field for backwards compatibility
|
||||
fileContent: z.string().optional().nullable(),
|
||||
// ... other params
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
// Auth always runs BEFORE parseRequest — never validate untrusted input before authenticating.
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const data = RequestSchema.parse(body)
|
||||
const parsed = await parseRequest({service}UploadContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
const data = parsed.data.body
|
||||
|
||||
let fileBuffer: Buffer
|
||||
let fileName: string
|
||||
@@ -625,7 +645,7 @@ export async function POST(request: NextRequest) {
|
||||
})
|
||||
|
||||
// ... handle response
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### 4. Update Tool to Use Internal Route
|
||||
|
||||
@@ -17,7 +17,7 @@ This codebase uses **emcn**, a custom component library built on Radix UI primit
|
||||
|
||||
## Steps
|
||||
|
||||
1. Read the emcn barrel export at `apps/sim/components/emcn/components/index.ts` to know what's available
|
||||
1. Read the emcn public barrel at `apps/sim/components/emcn/index.ts` (re-exports components, Calendar, Table*, and icons) to know what's available; for the full icon set read `apps/sim/components/emcn/icons/index.ts`
|
||||
2. Read `apps/sim/app/_styles/globals.css` for CSS variable tokens
|
||||
3. Analyze the specified scope against every rule below
|
||||
4. If fix=true, apply the fixes. If fix=false, propose the fixes without applying.
|
||||
@@ -34,18 +34,22 @@ This codebase uses **emcn**, a custom component library built on Radix UI primit
|
||||
|
||||
Use CSS variable pattern (`text-[var(--text-primary)]`), never Tailwind semantics (`text-muted-foreground`) or hardcoded colors (`text-gray-500`, `#333`).
|
||||
|
||||
**Text**: `--text-primary`, `--text-secondary`, `--text-tertiary`, `--text-muted`, `--text-icon`, `--text-inverse`, `--text-error`
|
||||
**Surfaces**: `--bg`, `--surface-2` through `--surface-7`, `--surface-hover`, `--surface-active`
|
||||
**Text**: `--text-primary`, `--text-secondary`, `--text-tertiary`, `--text-muted`, `--text-body` (canonical value text), `--text-icon`, `--text-placeholder`, `--text-subtle`, `--text-inverse`, `--text-error`
|
||||
**Surfaces**: `--bg`, `--surface-1` through `--surface-7`, `--surface-hover`, `--surface-active`
|
||||
**Borders**: `--border`, `--border-1`, `--border-muted`
|
||||
**Brand/accent**: `--brand-secondary`, `--brand-accent`
|
||||
**Z-Index**: `--z-dropdown` (100), `--z-modal` (200), `--z-popover` (300), `--z-tooltip` (400), `--z-toast` (500)
|
||||
**Shadows**: `shadow-subtle`, `shadow-medium`, `shadow-overlay`, `shadow-card`
|
||||
**Badges**: `--badge-*` semantic families (success/error/gray/blue/purple/orange/amber/teal/cyan/pink, each with `-bg`/`-text`)
|
||||
|
||||
## Buttons
|
||||
|
||||
Intent-to-variant mapping (read the actual `buttonVariants` in `apps/sim/components/emcn/components/button/button.tsx` for the full variant set — it exposes more than listed here):
|
||||
|
||||
| Action | Variant |
|
||||
|--------|---------|
|
||||
| Toolbar, icon-only | `ghost` (most common, 28%) |
|
||||
| Create, save, submit | `primary` (24%) |
|
||||
| Toolbar, icon-only | `ghost` |
|
||||
| Create, save, submit | `primary` |
|
||||
| Cancel, close | `default` |
|
||||
| Delete, remove | `destructive` |
|
||||
| Selected state | `active` |
|
||||
|
||||
@@ -153,7 +153,7 @@ This is the most common connector bug class — verify it explicitly against `sy
|
||||
- [ ] `title` is extracted from the correct field and has a sensible fallback (e.g., `'Untitled'`)
|
||||
- [ ] `content` is plain text — HTML content is stripped using `htmlToPlainText` from `@/connectors/utils`
|
||||
- [ ] `mimeType` is `'text/plain'`
|
||||
- [ ] `contentHash` is computed using `computeContentHash` from `@/connectors/utils`
|
||||
- [ ] `contentHash` is metadata-based for contentDeferred connectors (a string template like `service:${id}:${changeIndicator}`, identical between the `listDocuments` stub and `getDocument`); content-based via `computeContentHash` from `@/connectors/utils` ONLY when `listDocuments` returns full content inline
|
||||
- [ ] `sourceUrl` is a valid, complete URL back to the original resource (not relative)
|
||||
- [ ] `metadata` contains all fields referenced by `mapTags` and `tagDefinitions`
|
||||
|
||||
@@ -270,6 +270,7 @@ Group findings by severity:
|
||||
- Invalid scope names that the API doesn't recognize (even if silently ignored)
|
||||
- Private resources excluded from name-based lookup despite scopes being available
|
||||
- Silent data truncation without logging
|
||||
- `contentHash` uses the wrong basis: a content-based `computeContentHash` on a contentDeferred connector (breaks the stub/getDocument-identical invariant), or a metadata template when `listDocuments` returns full content inline
|
||||
- Size checks using `text.length` (character count) instead of `Buffer.byteLength` (byte count) for byte-based limits
|
||||
- URL-type config fields not normalized (protocol prefix, trailing slashes cause API failures)
|
||||
- `VALIDATE_RETRY_OPTIONS` not threaded through helper functions called by `validateConfig`
|
||||
|
||||
@@ -192,6 +192,12 @@ For **each tool** in `tools.access`:
|
||||
- [ ] `authMode` is set correctly (`AuthMode.OAuth` or `AuthMode.ApiKey`)
|
||||
- [ ] Block is registered in `blocks/registry.ts` alphabetically
|
||||
|
||||
### BlockMeta
|
||||
- [ ] `{Service}BlockMeta` is exported in the same file as the block
|
||||
- [ ] Has at least 7 templates, each with `icon`, `title`, `prompt`, `modules`, `category`, and `tags`
|
||||
- [ ] Prompts describe concrete use cases, not generic descriptions of what the service does
|
||||
- [ ] `alsoIntegrations` is set on any template whose prompt references another service
|
||||
|
||||
### Block Inputs
|
||||
- [ ] `inputs` section lists all subBlock params that the block accepts
|
||||
- [ ] Input types match the subBlock types
|
||||
@@ -282,6 +288,7 @@ After fixing, confirm:
|
||||
- [ ] Validated pagination consistency across tools and block
|
||||
- [ ] Validated error handling (error checks, meaningful messages)
|
||||
- [ ] Validated registry entries (tools and block, alphabetical, correct imports)
|
||||
- [ ] Validated `{Service}BlockMeta` exported with at least 7 templates
|
||||
- [ ] Reported all issues grouped by severity
|
||||
- [ ] Fixed all critical and warning issues
|
||||
- [ ] Ran `bun run lint` after fixes
|
||||
|
||||
@@ -5,32 +5,38 @@ paths:
|
||||
|
||||
# EMCN Components
|
||||
|
||||
Import from `@/components/emcn`, never from subpaths (except CSS files).
|
||||
Import from `@/components/emcn`, never from subpaths (except CSS files). The **chip family** is the platform's primary chrome — prefer it over the legacy primitives (`Input`, `Textarea`, etc.), which it is progressively replacing.
|
||||
|
||||
## CVA vs Direct Styles
|
||||
## Chip chrome — single source of truth
|
||||
|
||||
**Use CVA when:** 2+ variants (primary/secondary, sm/md/lg)
|
||||
Never hand-roll the chip pill from raw class strings (they go stale). Compose from the canonical sources:
|
||||
|
||||
```tsx
|
||||
const buttonVariants = cva('base-classes', {
|
||||
variants: { variant: { default: '...', primary: '...' } }
|
||||
})
|
||||
export { Button, buttonVariants }
|
||||
```
|
||||
- **Surface + typography tokens:** `chip-input/chip-field-chrome.ts` — `chipFilledSurfaceTokens`, `chipFieldSurfaceClass`, `chipFieldTextClass`. Text fields and the dropdown search box build on these.
|
||||
- **Pill geometry:** `chip/chip.tsx` — `chipVariants` (30px tall, `rounded-lg`, `px-2`, icon↔text `gap-1.5`). Every pill-shaped trigger (`ChipDropdown`, `ChipSelect`, `ChipSwitch`) reuses it for visual parity.
|
||||
|
||||
**Use direct className when:** Single consistent style, no variations
|
||||
Canonical look: normal font-weight (never `font-medium`/`font-semibold`), value text `--text-body`, icons `--text-icon` at `size-[14px]`, placeholder `--text-muted`, `transition-colors`, **no focus ring** (the caret marks focus). Filled surface is `--surface-5` light / `--surface-4` dark with a `--border-1` border.
|
||||
|
||||
```tsx
|
||||
function Label({ className, ...props }) {
|
||||
return <Primitive className={cn('style-classes', className)} {...props} />
|
||||
}
|
||||
```
|
||||
The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items use `text-small` and `gap-2` (a menu convention, not the chip pill). Keep them distinct.
|
||||
|
||||
## Rules
|
||||
## Component catalogue
|
||||
|
||||
- Use Radix UI primitives for accessibility
|
||||
- Export component and variants (if using CVA)
|
||||
- TSDoc with usage examples
|
||||
- Consistent tokens: `font-medium`, `text-[12px]`, `rounded-[4px]`
|
||||
- Equal height/width → `size-*` (e.g. `size-[14px]`, `size-4`), never `h-[Npx] w-[Npx]` or `h-N w-N`. Default icon size is `size-[14px]`
|
||||
- `transition-colors` for hover states
|
||||
- **`Chip` / `ChipLink`** — the pill button (`<button>` / Next `<Link>`). Variants: `ghost`, `filled`, `primary`, `destructive`, `border-shadow`. `leftIcon`/`rightIcon`, `active`, `fullWidth`, `flush`.
|
||||
- **`ChipInput`** — single-line text field. `icon`, `endAdornment`, `error`, `inputClassName` (inner `<input>`); `className` styles the chrome wrapper.
|
||||
- **`ChipTextarea`** — multi-line sibling. `error`, `resizable` (off by default).
|
||||
- **`ChipDropdown`** — pill that opens a menu. Single OR multi-select via the discriminated `multiple` prop (one component, not two). Owns its trailing chevron — no `rightIcon`.
|
||||
- **`ChipSelect` / `ChipCombobox`** — `Combobox`-backed pickers with search, groups, multi-select; for richer lists than `ChipDropdown`.
|
||||
- **`ChipModal` + `ChipModalField`** — declarative compact modal. The field's `type` (`input` | `email` | `textarea` | `dropdown` | `file` | `emails` | `custom`) picks the control and **owns all chrome** — consumers describe intent, never pass `variant`/`className`/`id` to the inner control. `custom` is the escape hatch.
|
||||
- **`ChipSwitch`** — segmented pill control (built from `chipVariants`).
|
||||
- **`ChipTag`** — 20px inline tag/badge (`mono`/`gray`/`invite`), not a pill trigger.
|
||||
- **`ChipDatePicker`** — chip-styled date field.
|
||||
|
||||
## Authoring principles
|
||||
|
||||
- **One source of truth for shared chrome.** Compose from `chip-field-chrome.ts` / `chipVariants`; never duplicate the chrome string.
|
||||
- **`cn()` for a single state toggle, CVA for genuine multiple variants.** A lone `error` boolean is `cn()`, not a CVA variant.
|
||||
- **Discriminated-union props for modes** (e.g. `multiple`, the modal field `type`) instead of near-duplicate components.
|
||||
- **Delete legacy variants after migration** — don't leave dead paths (this paradigm removed `Input variant='chip'` and `ChipMultiSelect`).
|
||||
- **Verify CSS vars exist.** An undefined var resolves to `currentColor` (caused a real black-border bug). Align to the canonical tokens: normal weight, `--text-body`, `--text-icon`.
|
||||
- Use Radix UI primitives for accessibility. Export the component and its `variants` (when using CVA). Document with TSDoc + a usage example.
|
||||
|
||||
Color tokens and icon-size conventions are canonical in `.claude/rules/sim-styling.md` — follow it rather than restating.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
paths:
|
||||
- "apps/sim/app/(home)/**/*.tsx"
|
||||
- "apps/sim/app/(landing)/**/*.tsx"
|
||||
---
|
||||
|
||||
# Landing Page — SEO / GEO
|
||||
@@ -21,6 +21,6 @@ paths:
|
||||
- **Answer-first pattern**: each section's H2 + subtitle should directly answer a user question (e.g. "What is Sim?", "How fast can I deploy?").
|
||||
- **Atomic answer blocks**: each feature / template card should be independently extractable by an AI summariser.
|
||||
- **Entity consistency**: always write "Sim" by name — never "the platform" or "our tool".
|
||||
- **Keyword density**: first 150 visible chars of Hero must name "Sim", "AI agents", "agentic workflows".
|
||||
- **Keyword density**: first 150 visible chars of Hero must name "Sim", "AI workspace", "AI agents".
|
||||
- **sr-only summaries**: Hero and Templates each have a `<p className="sr-only">` (~50 words) as an atomic product/catalog summary for AI citation.
|
||||
- **Specific numbers**: prefer concrete figures ("1,000+ integrations", "15+ AI providers") over vague claims.
|
||||
|
||||
@@ -14,19 +14,30 @@ paths:
|
||||
## Root-Level Structure
|
||||
|
||||
```
|
||||
apps/sim/
|
||||
├── app/ # Next.js app router (pages, API routes)
|
||||
├── blocks/ # Block definitions and registry
|
||||
├── components/ # Shared UI (emcn/, ui/)
|
||||
├── executor/ # Workflow execution engine
|
||||
├── hooks/ # Shared hooks (queries/, selectors/)
|
||||
├── lib/ # App-wide utilities
|
||||
├── providers/ # LLM provider integrations
|
||||
├── stores/ # Zustand stores
|
||||
├── tools/ # Tool definitions
|
||||
└── triggers/ # Trigger definitions
|
||||
apps/
|
||||
├── sim/ # this app (Next.js: UI + API routes + workflow editor)
|
||||
│ ├── app/ # Next.js app router (pages, API routes)
|
||||
│ ├── blocks/ # Block definitions and registry
|
||||
│ ├── components/ # Shared UI (emcn/, ui/)
|
||||
│ ├── executor/ # Workflow execution engine
|
||||
│ ├── hooks/ # Shared hooks (queries/, selectors/)
|
||||
│ ├── lib/ # App-wide utilities
|
||||
│ ├── providers/ # LLM provider integrations
|
||||
│ ├── stores/ # Zustand stores
|
||||
│ ├── tools/ # Tool definitions
|
||||
│ └── triggers/ # Trigger definitions
|
||||
└── realtime/ # Bun Socket.IO server (collaborative canvas)
|
||||
|
||||
packages/ # @sim/* — audit, auth, db, logger, realtime-protocol,
|
||||
# security, tsconfig, utils, workflow-authz,
|
||||
# workflow-persistence, workflow-types
|
||||
```
|
||||
|
||||
## Package Boundaries
|
||||
|
||||
- `apps/* → packages/*` only. Packages never import from `apps/*`.
|
||||
- `apps/realtime` avoids Next.js, React, the block/tool registry, provider SDKs, and the executor; never add `@/lib/webhooks/providers/*`, `@/executor/*`, `@/blocks/*`, or `@/tools/*` imports to any package it consumes. CI enforces this via `scripts/check-monorepo-boundaries.ts` and `scripts/check-realtime-prune-graph.ts`.
|
||||
|
||||
## Feature Organization
|
||||
|
||||
Features live under `app/workspace/[workspaceId]/`:
|
||||
|
||||
@@ -5,44 +5,10 @@ paths:
|
||||
|
||||
# Component Patterns
|
||||
|
||||
## Structure Order
|
||||
Component authoring rules — structure order (refs → external hooks → store hooks → custom hooks → state → useMemo → useCallback → useEffect → return), required props interface, and extraction thresholds (50+ lines / 2+ files vs keep inline < 10 lines) — live in CLAUDE.md > Components.
|
||||
|
||||
```typescript
|
||||
'use client' // Only if using hooks
|
||||
`.tsx`-specific deltas not covered there:
|
||||
|
||||
// Imports (external → internal)
|
||||
// Constants at module level
|
||||
const CONFIG = { SPACING: 8 } as const
|
||||
|
||||
// Props interface
|
||||
interface ComponentProps {
|
||||
requiredProp: string
|
||||
optionalProp?: boolean
|
||||
}
|
||||
|
||||
export function Component({ requiredProp, optionalProp = false }: ComponentProps) {
|
||||
// a. Refs
|
||||
// b. External hooks (useParams, useRouter)
|
||||
// c. Store hooks
|
||||
// d. Custom hooks
|
||||
// e. Local state
|
||||
// f. useMemo
|
||||
// g. useCallback
|
||||
// h. useEffect
|
||||
// i. Return JSX
|
||||
}
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
1. `'use client'` only when using React hooks
|
||||
2. Always define props interface
|
||||
3. Extract constants with `as const`
|
||||
4. Semantic HTML (`aside`, `nav`, `article`)
|
||||
5. Optional chain callbacks: `onAction?.(id)`
|
||||
|
||||
## Component Extraction
|
||||
|
||||
**Extract when:** 50+ lines, used in 2+ files, or has own state/logic
|
||||
|
||||
**Keep inline when:** < 10 lines, single use, purely presentational
|
||||
- `'use client'` only when using React hooks or browser-only APIs.
|
||||
- Prefer semantic HTML (`aside`, `nav`, `article`).
|
||||
- Optional-chain callbacks: `onAction?.(id)`.
|
||||
|
||||
+15
-20
@@ -8,40 +8,35 @@ paths:
|
||||
|
||||
## Structure
|
||||
|
||||
For server data, use a React Query hook from `hooks/queries/` — do NOT `useState` + `fetch` here (see `.claude/rules/sim-queries.md`). This pattern is for UI/orchestration hooks that hold UI-only state and wrap callbacks.
|
||||
|
||||
```typescript
|
||||
interface UseFeatureProps {
|
||||
id: string
|
||||
onSuccess?: (result: Result) => void
|
||||
onSelect?: (item: Item) => void
|
||||
}
|
||||
|
||||
export function useFeature({ id, onSuccess }: UseFeatureProps) {
|
||||
export function useFeature({ id, onSelect }: UseFeatureProps) {
|
||||
// 1. Refs for stable dependencies
|
||||
const idRef = useRef(id)
|
||||
const onSuccessRef = useRef(onSuccess)
|
||||
const onSelectRef = useRef(onSelect)
|
||||
|
||||
// 2. State
|
||||
const [data, setData] = useState<Data | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
// 2. UI-only state (never server data)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
// 3. Sync refs
|
||||
useEffect(() => {
|
||||
idRef.current = id
|
||||
onSuccessRef.current = onSuccess
|
||||
}, [id, onSuccess])
|
||||
onSelectRef.current = onSelect
|
||||
}, [id, onSelect])
|
||||
|
||||
// 4. Operations (useCallback with empty deps when using refs)
|
||||
const fetchData = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const result = await fetch(`/api/${idRef.current}`).then(r => r.json())
|
||||
setData(result)
|
||||
onSuccessRef.current?.(result)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
const select = useCallback((item: Item) => {
|
||||
onSelectRef.current?.(item)
|
||||
setIsOpen(false)
|
||||
}, [])
|
||||
|
||||
return { data, isLoading, fetchData }
|
||||
return { isOpen, setIsOpen, select }
|
||||
}
|
||||
```
|
||||
|
||||
@@ -51,5 +46,5 @@ export function useFeature({ id, onSuccess }: UseFeatureProps) {
|
||||
2. Props interface required
|
||||
3. Refs for stable callback dependencies
|
||||
4. Wrap returned functions in useCallback
|
||||
5. Always try/catch async operations
|
||||
6. Track loading/error states
|
||||
5. Server data goes through React Query (`hooks/queries/`), never `useState` + `fetch`
|
||||
6. Keep only UI/orchestration state in these hooks
|
||||
|
||||
@@ -7,281 +7,13 @@ paths:
|
||||
|
||||
# Adding Integrations
|
||||
|
||||
## Overview
|
||||
Build order: **Tools** (`tools/{service}/`) → **Block** (`blocks/blocks/{service}.ts`) → **Icon** (`components/icons.tsx`) → optional **Trigger** (`triggers/{service}/`). Always look up the service's API docs first.
|
||||
|
||||
Adding a new integration typically requires:
|
||||
1. **Tools** - API operations (`tools/{service}/`)
|
||||
2. **Block** - UI component (`blocks/blocks/{service}.ts`)
|
||||
3. **Icon** - SVG icon (`components/icons.tsx`)
|
||||
4. **Trigger** (optional) - Webhooks/polling (`triggers/{service}/`)
|
||||
The full authoring instructions — tool/block/icon/trigger scaffolding, SubBlock property tables, `condition`/`required`/`dependsOn`/`mode`/`canonicalParamId` syntax, the `normalizeFileInput`/file-handling helper table, and checklists — live in the skills: `/add-integration` (end-to-end), `/add-tools`, `/add-block`, `/add-trigger`.
|
||||
|
||||
Always look up the service's API docs first.
|
||||
## Hard rules (don't get these wrong)
|
||||
|
||||
## 1. Tools (`tools/{service}/`)
|
||||
|
||||
```
|
||||
tools/{service}/
|
||||
├── index.ts # Export all tools
|
||||
├── types.ts # Params/response types
|
||||
├── {action}.ts # Individual tool (e.g., send_message.ts)
|
||||
└── ...
|
||||
```
|
||||
|
||||
**Tool file structure:**
|
||||
|
||||
```typescript
|
||||
// tools/{service}/{action}.ts
|
||||
import type { {Service}Params, {Service}Response } from '@/tools/{service}/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const {service}{Action}Tool: ToolConfig<{Service}Params, {Service}Response> = {
|
||||
id: '{service}_{action}',
|
||||
name: '{Service} {Action}',
|
||||
description: 'What this tool does',
|
||||
version: '1.0.0',
|
||||
oauth: { required: true, provider: '{service}' }, // if OAuth
|
||||
params: { /* param definitions */ },
|
||||
request: {
|
||||
url: '/api/tools/{service}/{action}',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({ ...params }),
|
||||
},
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!data.success) throw new Error(data.error)
|
||||
return { success: true, output: data.output }
|
||||
},
|
||||
outputs: { /* output definitions */ },
|
||||
}
|
||||
```
|
||||
|
||||
**Register in `tools/registry.ts`:**
|
||||
|
||||
```typescript
|
||||
import { {service}{Action}Tool } from '@/tools/{service}'
|
||||
// Add to registry object
|
||||
{service}_{action}: {service}{Action}Tool,
|
||||
```
|
||||
|
||||
## 2. Block (`blocks/blocks/{service}.ts`)
|
||||
|
||||
```typescript
|
||||
import { {Service}Icon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import type { {Service}Response } from '@/tools/{service}/types'
|
||||
|
||||
export const {Service}Block: BlockConfig<{Service}Response> = {
|
||||
type: '{service}',
|
||||
name: '{Service}',
|
||||
description: 'Short description',
|
||||
longDescription: 'Detailed description',
|
||||
category: 'tools',
|
||||
bgColor: '#hexcolor',
|
||||
icon: {Service}Icon,
|
||||
subBlocks: [ /* see SubBlock Properties below */ ],
|
||||
tools: {
|
||||
access: ['{service}_{action}', ...],
|
||||
config: {
|
||||
tool: (params) => `{service}_${params.operation}`,
|
||||
params: (params) => ({ ...params }),
|
||||
},
|
||||
},
|
||||
inputs: { /* input definitions */ },
|
||||
outputs: { /* output definitions */ },
|
||||
}
|
||||
```
|
||||
|
||||
### SubBlock Properties
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'fieldName', // Unique identifier
|
||||
title: 'Field Label', // UI label
|
||||
type: 'short-input', // See SubBlock Types below
|
||||
placeholder: 'Hint text',
|
||||
required: true, // See Required below
|
||||
condition: { ... }, // See Condition below
|
||||
dependsOn: ['otherField'], // See DependsOn below
|
||||
mode: 'basic', // 'basic' | 'advanced' | 'both' | 'trigger'
|
||||
}
|
||||
```
|
||||
|
||||
**SubBlock Types:** `short-input`, `long-input`, `dropdown`, `code`, `switch`, `slider`, `oauth-input`, `channel-selector`, `user-selector`, `file-upload`, etc.
|
||||
|
||||
### `condition` - Show/hide based on another field
|
||||
|
||||
```typescript
|
||||
// Show when operation === 'send'
|
||||
condition: { field: 'operation', value: 'send' }
|
||||
|
||||
// Show when operation is 'send' OR 'read'
|
||||
condition: { field: 'operation', value: ['send', 'read'] }
|
||||
|
||||
// Show when operation !== 'send'
|
||||
condition: { field: 'operation', value: 'send', not: true }
|
||||
|
||||
// Complex: NOT in list AND another condition
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_channels', 'list_users'],
|
||||
not: true,
|
||||
and: { field: 'destinationType', value: 'dm', not: true }
|
||||
}
|
||||
```
|
||||
|
||||
### `required` - Field validation
|
||||
|
||||
```typescript
|
||||
// Always required
|
||||
required: true
|
||||
|
||||
// Conditionally required (same syntax as condition)
|
||||
required: { field: 'operation', value: 'send' }
|
||||
```
|
||||
|
||||
### `dependsOn` - Clear field when dependencies change
|
||||
|
||||
```typescript
|
||||
// Clear when credential changes
|
||||
dependsOn: ['credential']
|
||||
|
||||
// Clear when authMethod changes AND (credential OR botToken) changes
|
||||
dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] }
|
||||
```
|
||||
|
||||
### `mode` - When to show field
|
||||
|
||||
- `'basic'` - Only in basic mode (default UI)
|
||||
- `'advanced'` - Only in advanced mode (manual input)
|
||||
- `'both'` - Show in both modes (default)
|
||||
- `'trigger'` - Only when block is used as trigger
|
||||
|
||||
### `canonicalParamId` - Link basic/advanced alternatives
|
||||
|
||||
Use to map multiple UI inputs to a single logical parameter:
|
||||
|
||||
```typescript
|
||||
// Basic mode: Visual selector
|
||||
{
|
||||
id: 'fileSelector',
|
||||
type: 'file-selector',
|
||||
mode: 'basic',
|
||||
canonicalParamId: 'fileId',
|
||||
required: true,
|
||||
},
|
||||
// Advanced mode: Manual input
|
||||
{
|
||||
id: 'manualFileId',
|
||||
type: 'short-input',
|
||||
mode: 'advanced',
|
||||
canonicalParamId: 'fileId',
|
||||
required: true,
|
||||
},
|
||||
```
|
||||
|
||||
**Critical Rules:**
|
||||
- `canonicalParamId` must NOT match any subblock's `id`
|
||||
- `canonicalParamId` must be unique per operation/condition context
|
||||
- **Required consistency:** All subblocks in a canonical group must have the same `required` status
|
||||
- **Inputs section:** Must list canonical param IDs (e.g., `fileId`), NOT raw subblock IDs
|
||||
- **Params function:** Must use canonical param IDs (raw IDs are deleted after canonical transformation)
|
||||
|
||||
**Register in `blocks/registry.ts`:**
|
||||
|
||||
```typescript
|
||||
import { {Service}Block } from '@/blocks/blocks/{service}'
|
||||
// Add to registry object (alphabetically)
|
||||
{service}: {Service}Block,
|
||||
```
|
||||
|
||||
## 3. Icon (`components/icons.tsx`)
|
||||
|
||||
```typescript
|
||||
export function {Service}Icon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* SVG path from service's brand assets */}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Trigger (`triggers/{service}/`) - Optional
|
||||
|
||||
```
|
||||
triggers/{service}/
|
||||
├── index.ts # Export all triggers
|
||||
├── webhook.ts # Webhook handler
|
||||
├── utils.ts # Shared utilities
|
||||
└── {event}.ts # Specific event handlers
|
||||
```
|
||||
|
||||
**Register in `triggers/registry.ts`:**
|
||||
|
||||
```typescript
|
||||
import { {service}WebhookTrigger } from '@/triggers/{service}'
|
||||
// Add to TRIGGER_REGISTRY
|
||||
{service}_webhook: {service}WebhookTrigger,
|
||||
```
|
||||
|
||||
## File Handling
|
||||
|
||||
When integrations handle file uploads/downloads, use `UserFile` objects consistently.
|
||||
|
||||
### File Input (Uploads)
|
||||
|
||||
1. **Block subBlocks:** Use basic/advanced mode pattern with `canonicalParamId`
|
||||
2. **Normalize in block config:** Use `normalizeFileInput` from `@/blocks/utils`
|
||||
3. **Internal API route:** Create route that uses `downloadFileFromStorage` to get file content
|
||||
4. **Tool routes to internal API:** Don't call external APIs directly with files
|
||||
|
||||
```typescript
|
||||
// In block tools.config:
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
|
||||
const normalizedFile = normalizeFileInput(
|
||||
params.uploadFile || params.fileRef || params.fileContent,
|
||||
{ single: true }
|
||||
)
|
||||
if (normalizedFile) params.file = normalizedFile
|
||||
```
|
||||
|
||||
### File Output (Downloads)
|
||||
|
||||
Use `FileToolProcessor` in tool `transformResponse` to store files:
|
||||
|
||||
```typescript
|
||||
import { FileToolProcessor } from '@/executor/utils/file-tool-processor'
|
||||
|
||||
const processor = new FileToolProcessor(context)
|
||||
const file = await processor.processFileData({
|
||||
data: base64Content,
|
||||
mimeType: 'application/pdf',
|
||||
filename: 'doc.pdf',
|
||||
})
|
||||
```
|
||||
|
||||
### Key Helpers
|
||||
|
||||
| Helper | Location | Purpose |
|
||||
|--------|----------|---------|
|
||||
| `normalizeFileInput` | `@/blocks/utils` | Normalize file params in block config |
|
||||
| `processFilesToUserFiles` | `@/lib/uploads/utils/file-utils` | Convert raw inputs to UserFile[] |
|
||||
| `downloadFileFromStorage` | `@/lib/uploads/utils/file-utils.server` | Get Buffer from UserFile |
|
||||
| `FileToolProcessor` | `@/executor/utils/file-tool-processor` | Process tool output files |
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Look up API docs for the service
|
||||
- [ ] Create `tools/{service}/types.ts` with proper types
|
||||
- [ ] Create tool files for each operation
|
||||
- [ ] Create `tools/{service}/index.ts` barrel export
|
||||
- [ ] Register tools in `tools/registry.ts`
|
||||
- [ ] Add icon to `components/icons.tsx`
|
||||
- [ ] Create block in `blocks/blocks/{service}.ts`
|
||||
- [ ] Register block in `blocks/registry.ts`
|
||||
- [ ] (Optional) Create triggers in `triggers/{service}/`
|
||||
- [ ] (Optional) Register triggers in `triggers/registry.ts`
|
||||
- [ ] (If file uploads) Create internal API route with `downloadFileFromStorage`
|
||||
- [ ] (If file uploads) Use `normalizeFileInput` in block config
|
||||
- Tool IDs are `snake_case` (`service_action`). Register tools in `tools/registry.ts`, blocks in `blocks/registry.ts` (alphabetically), triggers in `triggers/registry.ts`.
|
||||
- Type coercions (`Number()`, etc.) belong in `tools.config.params` (runs at execution, after variable resolution) — never in `tools.config.tool` (runs at serialization; coercing there destroys dynamic `<Block.output>` references).
|
||||
- `canonicalParamId` must NOT match any subblock's `id`, must be unique per operation/condition context, and all subblocks in a canonical group must share the same `required` status. The `inputs` section and the params function reference canonical IDs, not raw subblock IDs.
|
||||
- Blocks must also set the catalog/UI metadata fields `integrationType`, `tags`, `authMode`, `docsLink`, and export a `{Service}BlockMeta` — see the `/add-block` skill's BlockMeta section for details.
|
||||
|
||||
@@ -37,12 +37,18 @@ Never use inline query keys — always use the factory.
|
||||
- Every `queryFn` must destructure and forward `signal` for request cancellation
|
||||
- Every query must have an explicit `staleTime`
|
||||
- Use `keepPreviousData` only on variable-key queries (where params change), never on static keys
|
||||
- Same-origin JSON calls must go through `requestJson(contract, ...)` from `@/lib/api/client/request` against the contract in `@/lib/api/contracts/**`
|
||||
|
||||
```typescript
|
||||
async function fetchEntities(workspaceId: string, signal?: AbortSignal) {
|
||||
const response = await fetch(`/api/entities?workspaceId=${workspaceId}`, { signal })
|
||||
if (!response.ok) throw new Error('Failed to fetch entities')
|
||||
return response.json()
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import { listEntitiesContract, type EntityList } from '@/lib/api/contracts/entities'
|
||||
|
||||
async function fetchEntities(workspaceId: string, signal?: AbortSignal): Promise<EntityList> {
|
||||
const data = await requestJson(listEntitiesContract, {
|
||||
query: { workspaceId },
|
||||
signal,
|
||||
})
|
||||
return data.entities
|
||||
}
|
||||
|
||||
export function useEntityList(workspaceId?: string, options?: { enabled?: boolean }) {
|
||||
@@ -60,12 +66,14 @@ export function useEntityList(workspaceId?: string, options?: { enabled?: boolea
|
||||
|
||||
- Use targeted invalidation (`entityKeys.lists()`) not broad (`entityKeys.all`) when possible
|
||||
- Invalidation must cover all affected query key prefixes (lists, details, related views)
|
||||
- Use `onSuccess` invalidation for plain mutations; use `onSettled` for optimistic mutations so the cache is reconciled on both success and error (see Optimistic Updates below)
|
||||
- `mutationFn` calls go through `requestJson(contract, { body, signal })` from `@/lib/api/client/request` — same boundary rule as queries
|
||||
|
||||
```typescript
|
||||
export function useCreateEntity() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (variables) => { /* fetch POST */ },
|
||||
mutationFn: (body: CreateEntityBody) => requestJson(createEntityContract, { body }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: entityKeys.lists() })
|
||||
},
|
||||
@@ -118,6 +126,11 @@ const handler = useCallback(() => {
|
||||
}, [data])
|
||||
```
|
||||
|
||||
## Boundary Types
|
||||
|
||||
- Hooks import named type aliases from `@/lib/api/contracts/**` (e.g., `import { listEntitiesContract, type EntityList } from '@/lib/api/contracts/entities'`). Never write `z.input<...>` / `z.output<...>` in hooks, and never `import { z } from 'zod'` in client code.
|
||||
- Raw `fetch` is allowed only for documented exceptions — multipart uploads, binary downloads, streaming responses, signed-URL flows, OAuth redirects, external origins. Each such raw `fetch(` inside `apps/sim/hooks/queries/**` or `apps/sim/hooks/selectors/**` — and any same-origin `/api/...` fetch elsewhere under `apps/sim/**` outside an API route handler — must be preceded by a `// boundary-raw-fetch: <reason>` annotation (reason non-empty; up to three preceding comment lines tolerated). Enforced by `scripts/check-api-validation-contracts.ts` (`bun run check:api-validation` / `:strict`).
|
||||
|
||||
## Naming
|
||||
|
||||
- **Keys**: `entityKeys`
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
description: Isolated-vm sandbox worker security policy. Hard rules for anything that lives in the worker child process that runs user code.
|
||||
paths:
|
||||
- "apps/sim/lib/execution/isolated-vm-worker.cjs"
|
||||
- "apps/sim/lib/execution/isolated-vm.ts"
|
||||
- "apps/sim/lib/execution/sandbox/**"
|
||||
- "apps/sim/sandbox-tasks/**"
|
||||
---
|
||||
|
||||
# Sim Sandbox — Worker Security Policy
|
||||
|
||||
The isolated-vm worker child process at
|
||||
`apps/sim/lib/execution/isolated-vm-worker.cjs` runs untrusted user code inside
|
||||
V8 isolates. The process itself is a trust boundary. Everything in this rule is
|
||||
about what must **never** live in that process.
|
||||
|
||||
## Hard rules
|
||||
|
||||
1. **No app credentials in the worker process**. The worker must not hold, load,
|
||||
or receive via IPC: database URLs, Redis URLs, AWS keys, Stripe keys,
|
||||
session-signing keys, encryption keys, OAuth client secrets, internal API
|
||||
secrets, or any LLM / email / search provider API keys. If you catch yourself
|
||||
`require`'ing `@/lib/auth`, `@sim/db`, `@/lib/uploads/core/storage-service`,
|
||||
or anything that imports `env` directly inside the worker, stop and use a
|
||||
host-side broker instead.
|
||||
|
||||
2. **Host-side brokers own all credentialed work**. The worker can only access
|
||||
resources through `ivm.Reference` / `ivm.Callback` bridges back to the host
|
||||
process. Today the only broker is `workspaceFileBroker`
|
||||
(`apps/sim/lib/execution/sandbox/brokers/workspace-file.ts`); adding a new
|
||||
one requires co-reviewing this file.
|
||||
|
||||
3. **Host-side brokers must scope every resource access to a single tenant**.
|
||||
The `SandboxBrokerContext` always carries `workspaceId`. Any new broker that
|
||||
accesses storage, DB, or an external API must use `ctx.workspaceId` to scope
|
||||
the lookup — never accept a raw path, key, or URL from isolate code without
|
||||
validation.
|
||||
|
||||
4. **Nothing that runs in the isolate is trusted, even if we wrote it**. The
|
||||
task `bootstrap` and `finalize` strings in `apps/sim/sandbox-tasks/` execute
|
||||
inside the isolate. They must treat `globalThis` as adversarial — no pulling
|
||||
values from it that might have been mutated by user code. The hardening
|
||||
script in `executeTask` undefines dangerous globals before user code runs.
|
||||
|
||||
## Why
|
||||
|
||||
A V8 JIT bug (Chrome ships these roughly monthly) gives an attacker a native
|
||||
code primitive inside the process that owns whatever that process can reach.
|
||||
If the worker only holds `isolated-vm` + a single narrow workspace-file broker,
|
||||
a V8 escape leaks one tenant's files. If the worker holds a Stripe key or a DB
|
||||
connection, a V8 escape leaks the service.
|
||||
|
||||
The original `doc-worker.cjs` vulnerability (CVE-class, 225 production secrets
|
||||
leaked via `/proc/1/environ`) was the forcing function for this architecture.
|
||||
Keep the blast radius small.
|
||||
|
||||
## Checklist for changes to `isolated-vm-worker.cjs`
|
||||
|
||||
Before landing any change that adds a new `require(...)` or `process.send(...)`
|
||||
payload or `ivm.Reference` wrapper in the worker:
|
||||
|
||||
- [ ] Does it load a credential, key, connection string, or secret? If yes,
|
||||
move it host-side and expose as a broker.
|
||||
- [ ] Does it import from `@/lib/auth`, `@sim/db`, `@/lib/uploads/core/*`,
|
||||
`@/lib/core/config/env`, or any module that reads `process.env` of the
|
||||
main app? If yes, same — move host-side.
|
||||
- [ ] Does it expose a resource that's workspace-scoped without taking a
|
||||
`workspaceId`? If yes, re-scope.
|
||||
- [ ] Did you update the broker limits (`IVM_MAX_BROKER_ARGS_JSON_CHARS`,
|
||||
`IVM_MAX_BROKER_RESULT_JSON_CHARS`, `IVM_MAX_BROKERS_PER_EXECUTION`) if
|
||||
the new broker can emit large payloads or fire frequently?
|
||||
|
||||
## What the worker *may* hold
|
||||
|
||||
- `isolated-vm` module
|
||||
- Node built-ins: `node:fs` (only for reading the checked-in bundle `.cjs`
|
||||
files) and `node:path`
|
||||
- The three prebuilt library bundles under
|
||||
`apps/sim/lib/execution/sandbox/bundles/*.cjs`
|
||||
- IPC message handlers for `execute`, `cancel`, `fetchResponse`,
|
||||
`brokerResponse`
|
||||
|
||||
The worker deliberately has **no host-side logger**. All errors and
|
||||
diagnostics flow through IPC back to the host, which has `@sim/logger`. Do
|
||||
not add `createLogger` or console-based logging to the worker — it would
|
||||
require pulling the main app's config / env, which is exactly what this
|
||||
rule is preventing.
|
||||
|
||||
Anything else is suspect.
|
||||
@@ -11,13 +11,12 @@ paths:
|
||||
1. **No inline styles** - Use Tailwind classes
|
||||
2. **No duplicate dark classes** - Skip `dark:` when value matches light mode
|
||||
3. **Exact values** - `text-[14px]`, `h-[26px]`
|
||||
4. **Equal h/w → `size-*`** - Use `size-[14px]` / `size-4`, never `h-[14px] w-[14px]` or `h-4 w-4`. Default icon size is `size-[14px]`
|
||||
5. **Transitions** - `transition-colors` for interactive states
|
||||
4. **Transitions** - `transition-colors` for interactive states
|
||||
|
||||
## Conditional Classes
|
||||
|
||||
```typescript
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
|
||||
<div className={cn(
|
||||
'base-classes',
|
||||
@@ -40,3 +39,38 @@ setWidth: (width) => {
|
||||
// In component
|
||||
<aside style={{ width: 'var(--sidebar-width)' }} />
|
||||
```
|
||||
|
||||
## Text Scale
|
||||
|
||||
Custom font sizes (`apps/sim/tailwind.config.ts`): `text-micro`=10px, `text-xs`=11px, `text-caption`=12px, `text-small`=13px, `text-base`=15px. `text-sm` is Tailwind default 14px. Field titles use `text-small` (13px); hints/errors use `text-caption` (12px).
|
||||
|
||||
Icons default `size-[14px]`. Equal h/w → `size-*` (`size-[14px]`, `size-4`), never `h-N w-N`.
|
||||
|
||||
## Color Tokens
|
||||
|
||||
Value text `--text-body`; muted/placeholder/labels `--text-muted`; icons `--text-icon`; borders `--border-1` (fields) / `--border` (dividers); surfaces `--surface-5` (light) / `--surface-4` (dark); active row `--surface-active`; error `--text-error`. No focus rings on chip surfaces.
|
||||
|
||||
## Chip Components (consumer usage)
|
||||
|
||||
`ChipInput`, `ChipTextarea`, `ChipModal*` own their full chrome. Consumers describe intent through PROPS; they never re-style the chrome. The canonical chrome lives in `apps/sim/components/emcn/components/chip-input/chip-field-chrome.ts` — never hand-roll `rounded-lg`/`border`/`bg-[var(--surface-5)]`/`h-[30px]`/`px-2`/`text-sm`/focus rings.
|
||||
|
||||
### Props over className
|
||||
|
||||
- **Errors** → `error` prop. Never `className={cn(err && 'border-[var(--text-error)]')}`.
|
||||
- **Leading icon** → `icon` prop (rendered 14px in `--text-icon`).
|
||||
- **Trailing buttons** (reveal/copy/fetch) → `endAdornment`.
|
||||
- **Inner-input styling** (e.g. `font-mono`, number-spinner reset) → `inputClassName` (ChipInput only). See `app/workspace/[workspaceId]/settings/components/billing/components/usage-limit-field/usage-limit-field.tsx:117`.
|
||||
- **`ChipModalField` controls take NO className.** Pass `title`/`value`/`onChange`/`error`/`hint`/`required`/`flush`. The field owns label, control, and error/hint rendering. See `app/workspace/[workspaceId]/skills/components/skill-modal/skill-modal.tsx:170`.
|
||||
|
||||
### What className MAY carry
|
||||
|
||||
Layout/sizing ONLY: `flex-1`, `w-full`, `w-[Npx]`, `min-w-0`, `max-w-*`, margins, `truncate`. Example: `<ChipInput icon={Search} className='min-w-0 flex-1' .../>` (`app/workspace/[workspaceId]/integrations/integrations.tsx:257`). NEVER re-specify canonical chrome — the component already applies it.
|
||||
|
||||
### Form / chip-modal layout rhythm
|
||||
|
||||
- **Field row** = `ChipModalField`: label↔control `gap-[9px]`, field gutter `px-2` (`px-0` when `flush`). Title = `Label` at `text-small` (13px), muted, normal weight; hint/error at `text-caption` (12px).
|
||||
- **Modal body** (`ChipModalBody`): `gap-4` between fields, padding `px-2 pt-4 pb-4.5`.
|
||||
- **Header/footer**: horizontal gutter `px-4` (header `pt-3`; footer `px-4 pt-2 pb-2`, tinted bar).
|
||||
- **Page section rhythm** (integrations/skills/settings): muted `text-small` label + `mt-[9px] mb-3 h-px bg-[var(--border)]` divider, sections stacked `gap-7`. Reuse `SettingsSection` (`app/workspace/[workspaceId]/settings/components/settings-section/settings-section.tsx`) rather than re-deriving it.
|
||||
|
||||
When a standalone labeled field outside a `ChipModal` needs the same look (e.g. `SkillImport`), match the field rhythm by hand: `flex flex-col gap-[9px]`, muted label, `ChipInput`/`ChipTextarea` control, `text-caption` error below.
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
---
|
||||
paths:
|
||||
- "apps/sim/**/*.test.ts"
|
||||
- "apps/sim/**/*.test.tsx"
|
||||
---
|
||||
|
||||
# Testing Patterns
|
||||
|
||||
Use Vitest. Test files: `feature.ts` → `feature.test.ts`
|
||||
|
||||
## Global Mocks (vitest.setup.ts)
|
||||
|
||||
These modules are mocked globally — do NOT re-mock them in test files unless you need to override behavior:
|
||||
|
||||
- `@sim/db` → `databaseMock`
|
||||
- `@sim/db/schema` → `schemaMock`
|
||||
- `drizzle-orm` → `drizzleOrmMock`
|
||||
- `@sim/logger` → `loggerMock`
|
||||
- `@/lib/auth` → `authMock`
|
||||
- `@/lib/auth/hybrid` → `hybridAuthMock` (with default session-delegating behavior)
|
||||
- `@/lib/core/utils/request` → `requestUtilsMock`
|
||||
- `@/stores/console/store`, `@/stores/terminal`, `@/stores/execution/store`
|
||||
- `@/blocks/registry`
|
||||
- `@trigger.dev/sdk`
|
||||
|
||||
## Structure
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { createMockRequest } from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { mockGetSession } = vi.hoisted(() => ({
|
||||
mockGetSession: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth', () => ({
|
||||
auth: { api: { getSession: vi.fn() } },
|
||||
getSession: mockGetSession,
|
||||
}))
|
||||
|
||||
import { GET, POST } from '@/app/api/my-route/route'
|
||||
|
||||
describe('my route', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
|
||||
})
|
||||
|
||||
it('returns data', async () => {
|
||||
const req = createMockRequest('GET')
|
||||
const res = await GET(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Performance Rules (Critical)
|
||||
|
||||
### NEVER use `vi.resetModules()` + `vi.doMock()` + `await import()`
|
||||
|
||||
This is the #1 cause of slow tests. It forces complete module re-evaluation per test.
|
||||
|
||||
```typescript
|
||||
// BAD — forces module re-evaluation every test (~50-100ms each)
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
vi.doMock('@/lib/auth', () => ({ getSession: vi.fn() }))
|
||||
})
|
||||
it('test', async () => {
|
||||
const { GET } = await import('./route') // slow dynamic import
|
||||
})
|
||||
|
||||
// GOOD — module loaded once, mocks reconfigured per test (~1ms each)
|
||||
const { mockGetSession } = vi.hoisted(() => ({
|
||||
mockGetSession: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/lib/auth', () => ({ getSession: mockGetSession }))
|
||||
import { GET } from '@/app/api/my-route/route'
|
||||
|
||||
beforeEach(() => { vi.clearAllMocks() })
|
||||
it('test', () => {
|
||||
mockGetSession.mockResolvedValue({ user: { id: '1' } })
|
||||
})
|
||||
```
|
||||
|
||||
**Only exception:** Singleton modules that cache state at module scope (e.g., Redis clients, connection pools). These genuinely need `vi.resetModules()` + dynamic import to get a fresh instance per test.
|
||||
|
||||
### NEVER use `vi.importActual()`
|
||||
|
||||
This defeats the purpose of mocking by loading the real module and all its dependencies.
|
||||
|
||||
```typescript
|
||||
// BAD — loads real module + all transitive deps
|
||||
vi.mock('@/lib/workspaces/utils', async () => {
|
||||
const actual = await vi.importActual('@/lib/workspaces/utils')
|
||||
return { ...actual, myFn: vi.fn() }
|
||||
})
|
||||
|
||||
// GOOD — mock everything, only implement what tests need
|
||||
vi.mock('@/lib/workspaces/utils', () => ({
|
||||
myFn: vi.fn(),
|
||||
otherFn: vi.fn(),
|
||||
}))
|
||||
```
|
||||
|
||||
### Mock heavy transitive dependencies
|
||||
|
||||
If a module under test imports `@/blocks` (200+ files), `@/tools/registry`, or other heavy modules, mock them:
|
||||
|
||||
```typescript
|
||||
vi.mock('@/blocks', () => ({
|
||||
getBlock: () => null,
|
||||
getAllBlocks: () => ({}),
|
||||
getAllBlockTypes: () => [],
|
||||
registry: {},
|
||||
}))
|
||||
```
|
||||
|
||||
### Use `@vitest-environment node` unless DOM is needed
|
||||
|
||||
Only use `@vitest-environment jsdom` if the test uses `window`, `document`, `FormData`, or other browser APIs. Node environment is significantly faster.
|
||||
|
||||
### Avoid real timers in tests
|
||||
|
||||
```typescript
|
||||
// BAD
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
// GOOD — use minimal delays or fake timers
|
||||
await new Promise(r => setTimeout(r, 1))
|
||||
// or
|
||||
vi.useFakeTimers()
|
||||
```
|
||||
|
||||
## Centralized Mocks (prefer over local declarations)
|
||||
|
||||
`@sim/testing` exports ready-to-use mock modules for common dependencies. Import and pass directly to `vi.mock()` — no `vi.hoisted()` boilerplate needed. Each paired `*MockFns` object exposes the underlying `vi.fn()`s for per-test overrides.
|
||||
|
||||
| Module mocked | Import | Factory form |
|
||||
|---|---|---|
|
||||
| `@/app/api/auth/oauth/utils` | `authOAuthUtilsMock`, `authOAuthUtilsMockFns` | `vi.mock('@/app/api/auth/oauth/utils', () => authOAuthUtilsMock)` |
|
||||
| `@/app/api/knowledge/utils` | `knowledgeApiUtilsMock`, `knowledgeApiUtilsMockFns` | `vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock)` |
|
||||
| `@/app/api/workflows/utils` | `workflowsApiUtilsMock`, `workflowsApiUtilsMockFns` | `vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock)` |
|
||||
| `@sim/audit` | `auditMock`, `auditMockFns` | `vi.mock('@sim/audit', () => auditMock)` |
|
||||
| `@/lib/auth` | `authMock`, `authMockFns` | `vi.mock('@/lib/auth', () => authMock)` |
|
||||
| `@/lib/auth/hybrid` | `hybridAuthMock`, `hybridAuthMockFns` | `vi.mock('@/lib/auth/hybrid', () => hybridAuthMock)` |
|
||||
| `@/lib/copilot/request/http` | `copilotHttpMock`, `copilotHttpMockFns` | `vi.mock('@/lib/copilot/request/http', () => copilotHttpMock)` |
|
||||
| `@/lib/core/config/env` | `envMock`, `createEnvMock(overrides)` | `vi.mock('@/lib/core/config/env', () => envMock)` |
|
||||
| `@/lib/core/config/feature-flags` | `featureFlagsMock` | `vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock)` |
|
||||
| `@/lib/core/config/redis` | `redisConfigMock`, `redisConfigMockFns` | `vi.mock('@/lib/core/config/redis', () => redisConfigMock)` |
|
||||
| `@/lib/core/security/encryption` | `encryptionMock`, `encryptionMockFns` | `vi.mock('@/lib/core/security/encryption', () => encryptionMock)` |
|
||||
| `@/lib/core/security/input-validation.server` | `inputValidationMock`, `inputValidationMockFns` | `vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock)` |
|
||||
| `@/lib/core/utils/request` | `requestUtilsMock`, `requestUtilsMockFns` | `vi.mock('@/lib/core/utils/request', () => requestUtilsMock)` |
|
||||
| `@/lib/core/utils/urls` | `urlsMock`, `urlsMockFns` | `vi.mock('@/lib/core/utils/urls', () => urlsMock)` |
|
||||
| `@/lib/execution/preprocessing` | `executionPreprocessingMock`, `executionPreprocessingMockFns` | `vi.mock('@/lib/execution/preprocessing', () => executionPreprocessingMock)` |
|
||||
| `@/lib/logs/execution/logging-session` | `loggingSessionMock`, `loggingSessionMockFns`, `LoggingSessionMock` | `vi.mock('@/lib/logs/execution/logging-session', () => loggingSessionMock)` |
|
||||
| `@/lib/workflows/orchestration` | `workflowsOrchestrationMock`, `workflowsOrchestrationMockFns` | `vi.mock('@/lib/workflows/orchestration', () => workflowsOrchestrationMock)` |
|
||||
| `@/lib/workflows/persistence/utils` | `workflowsPersistenceUtilsMock`, `workflowsPersistenceUtilsMockFns` | `vi.mock('@/lib/workflows/persistence/utils', () => workflowsPersistenceUtilsMock)` |
|
||||
| `@/lib/workflows/utils` | `workflowsUtilsMock`, `workflowsUtilsMockFns` | `vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock)` |
|
||||
| `@/lib/workspaces/permissions/utils` | `permissionsMock`, `permissionsMockFns` | `vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock)` |
|
||||
| `@sim/db/schema` | `schemaMock` | `vi.mock('@sim/db/schema', () => schemaMock)` |
|
||||
|
||||
### Auth mocking (API routes)
|
||||
|
||||
```typescript
|
||||
import { authMock, authMockFns } from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@/lib/auth', () => authMock)
|
||||
|
||||
import { GET } from '@/app/api/my-route/route'
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
|
||||
})
|
||||
```
|
||||
|
||||
Only define a local `vi.mock('@/lib/auth', ...)` if the module under test consumes exports outside the centralized shape (e.g., `auth.api.verifyOneTimeToken`, `auth.api.resetPassword`).
|
||||
|
||||
### Hybrid auth mocking
|
||||
|
||||
```typescript
|
||||
import { hybridAuthMock, hybridAuthMockFns } from '@sim/testing'
|
||||
|
||||
vi.mock('@/lib/auth/hybrid', () => hybridAuthMock)
|
||||
|
||||
// In tests:
|
||||
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
|
||||
success: true, userId: 'user-1', authType: 'session',
|
||||
})
|
||||
```
|
||||
|
||||
### Database chain mocking
|
||||
|
||||
Use the centralized `dbChainMock` + `dbChainMockFns` helpers — no `vi.hoisted()` or chain-wiring boilerplate needed.
|
||||
|
||||
```typescript
|
||||
import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing'
|
||||
|
||||
vi.mock('@sim/db', () => dbChainMock)
|
||||
// Spread for custom exports: vi.mock('@sim/db', () => ({ ...dbChainMock, myTable: {...} }))
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
resetDbChainMock() // only needed if tests use permanent (non-`Once`) overrides
|
||||
})
|
||||
|
||||
it('reads a row', async () => {
|
||||
dbChainMockFns.limit.mockResolvedValueOnce([{ id: '1', name: 'test' }])
|
||||
// exercise code that hits db.select().from().where().limit()
|
||||
expect(dbChainMockFns.where).toHaveBeenCalled()
|
||||
})
|
||||
```
|
||||
|
||||
**Default chains supported:**
|
||||
- `select()/selectDistinct()/selectDistinctOn() → from() → where()/innerJoin()/leftJoin() → where() → limit()/orderBy()/returning()/groupBy()/for()`
|
||||
- `insert() → values() → returning()/onConflictDoUpdate()/onConflictDoNothing()`
|
||||
- `update() → set() → where() → limit()/orderBy()/returning()/for()`
|
||||
- `delete() → where() → limit()/orderBy()/returning()/for()`
|
||||
- `db.execute()` resolves `[]`
|
||||
- `db.transaction(cb)` calls cb with `dbChainMock.db`
|
||||
|
||||
`.for('update')` (Postgres row-level locking) is supported on `where`
|
||||
builders. It returns a thenable with `.limit` / `.orderBy` / `.returning` /
|
||||
`.groupBy` attached, so both `await .where().for('update')` (terminal) and
|
||||
`await .where().for('update').limit(1)` (chained) work. Override the terminal
|
||||
result with `dbChainMockFns.for.mockResolvedValueOnce([...])`; for the chained
|
||||
form, mock the downstream terminal (e.g. `dbChainMockFns.limit.mockResolvedValueOnce([...])`).
|
||||
|
||||
All terminals default to `Promise.resolve([])`. Override per-test with `dbChainMockFns.<terminal>.mockResolvedValueOnce(...)`.
|
||||
|
||||
Use `resetDbChainMock()` in `beforeEach` only when tests replace wiring with `.mockReturnValue` / `.mockResolvedValue` (permanent). Tests using only `...Once` variants don't need it.
|
||||
|
||||
## @sim/testing Package
|
||||
|
||||
Always prefer over local test data.
|
||||
|
||||
| Category | Utilities |
|
||||
|----------|-----------|
|
||||
| **Module mocks** | See "Centralized Mocks" table above |
|
||||
| **Logger helpers** | `loggerMock`, `createMockLogger()`, `getLoggerCalls()`, `clearLoggerMocks()` |
|
||||
| **Database helpers** | `databaseMock`, `drizzleOrmMock`, `createMockDb()`, `createMockSql()`, `createMockSqlOperators()` |
|
||||
| **Fetch helpers** | `setupGlobalFetchMock()`, `createMockFetch()`, `createMockResponse()`, `mockFetchError()` |
|
||||
| **Factories** | `createSession()`, `createWorkflowRecord()`, `createBlock()`, `createExecutionContext()` |
|
||||
| **Builders** | `WorkflowBuilder`, `ExecutionContextBuilder` |
|
||||
| **Assertions** | `expectWorkflowAccessGranted()`, `expectBlockExecuted()` |
|
||||
| **Requests** | `createMockRequest()`, `createMockFormDataRequest()` |
|
||||
|
||||
## Rules Summary
|
||||
|
||||
1. `@vitest-environment node` unless DOM is required
|
||||
2. Prefer centralized mocks from `@sim/testing` (see table above) over local `vi.hoisted()` + `vi.mock()` boilerplate
|
||||
3. `vi.hoisted()` + `vi.mock()` + static imports — never `vi.resetModules()` + `vi.doMock()` + dynamic imports
|
||||
4. `vi.mock()` calls before importing mocked modules
|
||||
5. `beforeEach(() => vi.clearAllMocks())` to reset state — no redundant `afterEach`
|
||||
6. No `vi.importActual()` — mock everything explicitly
|
||||
7. Mock heavy deps (`@/blocks`, `@/tools/registry`, `@/triggers`) in tests that don't need them
|
||||
8. Use absolute imports in test files
|
||||
9. Avoid real timers — use 1ms delays or `vi.useFakeTimers()`
|
||||
@@ -2,34 +2,40 @@
|
||||
description: EMCN component library patterns
|
||||
globs: ["apps/sim/components/emcn/**"]
|
||||
---
|
||||
|
||||
# EMCN Components
|
||||
|
||||
Import from `@/components/emcn`, never from subpaths (except CSS files).
|
||||
Import from `@/components/emcn`, never from subpaths (except CSS files). The **chip family** is the platform's primary chrome — prefer it over the legacy primitives (`Input`, `Textarea`, etc.), which it is progressively replacing.
|
||||
|
||||
## CVA vs Direct Styles
|
||||
## Chip chrome — single source of truth
|
||||
|
||||
**Use CVA when:** 2+ variants (primary/secondary, sm/md/lg)
|
||||
Never hand-roll the chip pill from raw class strings (they go stale). Compose from the canonical sources:
|
||||
|
||||
```tsx
|
||||
const buttonVariants = cva('base-classes', {
|
||||
variants: { variant: { default: '...', primary: '...' } }
|
||||
})
|
||||
export { Button, buttonVariants }
|
||||
```
|
||||
- **Surface + typography tokens:** `chip-input/chip-field-chrome.ts` — `chipFilledSurfaceTokens`, `chipFieldSurfaceClass`, `chipFieldTextClass`. Text fields and the dropdown search box build on these.
|
||||
- **Pill geometry:** `chip/chip.tsx` — `chipVariants` (30px tall, `rounded-lg`, `px-2`, icon↔text `gap-1.5`). Every pill-shaped trigger (`ChipDropdown`, `ChipSelect`, `ChipSwitch`) reuses it for visual parity.
|
||||
|
||||
**Use direct className when:** Single consistent style, no variations
|
||||
Canonical look: normal font-weight (never `font-medium`/`font-semibold`), value text `--text-body`, icons `--text-icon` at `size-[14px]`, placeholder `--text-muted`, `transition-colors`, **no focus ring** (the caret marks focus). Filled surface is `--surface-5` light / `--surface-4` dark with a `--border-1` border.
|
||||
|
||||
```tsx
|
||||
function Label({ className, ...props }) {
|
||||
return <Primitive className={cn('style-classes', className)} {...props} />
|
||||
}
|
||||
```
|
||||
The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items use `text-small` and `gap-2` (a menu convention, not the chip pill). Keep them distinct.
|
||||
|
||||
## Rules
|
||||
## Component catalogue
|
||||
|
||||
- Use Radix UI primitives for accessibility
|
||||
- Export component and variants (if using CVA)
|
||||
- TSDoc with usage examples
|
||||
- Consistent tokens: `font-medium`, `text-[12px]`, `rounded-[4px]`
|
||||
- `transition-colors` for hover states
|
||||
- **`Chip` / `ChipLink`** — the pill button (`<button>` / Next `<Link>`). Variants: `ghost`, `filled`, `primary`, `destructive`, `border-shadow`. `leftIcon`/`rightIcon`, `active`, `fullWidth`, `flush`.
|
||||
- **`ChipInput`** — single-line text field. `icon`, `endAdornment`, `error`, `inputClassName` (inner `<input>`); `className` styles the chrome wrapper.
|
||||
- **`ChipTextarea`** — multi-line sibling. `error`, `resizable` (off by default).
|
||||
- **`ChipDropdown`** — pill that opens a menu. Single OR multi-select via the discriminated `multiple` prop (one component, not two). Owns its trailing chevron — no `rightIcon`.
|
||||
- **`ChipSelect` / `ChipCombobox`** — `Combobox`-backed pickers with search, groups, multi-select; for richer lists than `ChipDropdown`.
|
||||
- **`ChipModal` + `ChipModalField`** — declarative compact modal. The field's `type` (`input` | `email` | `textarea` | `dropdown` | `file` | `emails` | `custom`) picks the control and **owns all chrome** — consumers describe intent, never pass `variant`/`className`/`id` to the inner control. `custom` is the escape hatch.
|
||||
- **`ChipSwitch`** — segmented pill control (built from `chipVariants`).
|
||||
- **`ChipTag`** — 20px inline tag/badge (`mono`/`gray`/`invite`), not a pill trigger.
|
||||
- **`ChipDatePicker`** — chip-styled date field.
|
||||
|
||||
## Authoring principles
|
||||
|
||||
- **One source of truth for shared chrome.** Compose from `chip-field-chrome.ts` / `chipVariants`; never duplicate the chrome string.
|
||||
- **`cn()` for a single state toggle, CVA for genuine multiple variants.** A lone `error` boolean is `cn()`, not a CVA variant.
|
||||
- **Discriminated-union props for modes** (e.g. `multiple`, the modal field `type`) instead of near-duplicate components.
|
||||
- **Delete legacy variants after migration** — don't leave dead paths (this paradigm removed `Input variant='chip'` and `ChipMultiSelect`).
|
||||
- **Verify CSS vars exist.** An undefined var resolves to `currentColor` (caused a real black-border bug). Align to the canonical tokens: normal weight, `--text-body`, `--text-icon`.
|
||||
- Use Radix UI primitives for accessibility. Export the component and its `variants` (when using CVA). Document with TSDoc + a usage example.
|
||||
|
||||
Color tokens and icon-size conventions are canonical in `.claude/rules/sim-styling.md` — follow it rather than restating.
|
||||
|
||||
+21
-12
@@ -2,13 +2,13 @@
|
||||
description: Global coding standards that apply to all files
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Global Standards
|
||||
|
||||
You are a professional software engineer. All code must follow best practices: accurate, readable, clean, and efficient.
|
||||
|
||||
## Logging
|
||||
Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`.
|
||||
Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`. Inside API routes wrapped with `withRouteHandler`, loggers automatically include the request ID.
|
||||
|
||||
## API Route Handlers
|
||||
All API route handlers must be wrapped with `withRouteHandler` from `@/lib/core/utils/with-route-handler`. Never export a bare `async function GET/POST/...` — always use `export const METHOD = withRouteHandler(...)`.
|
||||
|
||||
## Comments
|
||||
Use TSDoc for documentation. No `====` separators. No non-TSDoc comments.
|
||||
@@ -40,22 +40,31 @@ const tiny = generateShortId(8)
|
||||
## Common Utilities
|
||||
Use shared helpers from `@sim/utils` instead of writing inline implementations:
|
||||
|
||||
- `sleep(ms)` — async delay. Never write `new Promise(resolve => setTimeout(resolve, ms))`
|
||||
- `toError(value)` — normalize unknown caught values to `Error`. Never write `e instanceof Error ? e : new Error(String(e))`
|
||||
- `toError(value).message` — get error message safely. Never write `e instanceof Error ? e.message : String(e)`
|
||||
- `sleep(ms)` from `@sim/utils/helpers` — async delay. Never write `new Promise(resolve => setTimeout(resolve, ms))`
|
||||
- `toError(value)` from `@sim/utils/errors` — normalize unknown caught values to `Error`. Never write `e instanceof Error ? e : new Error(String(e))`
|
||||
- `getErrorMessage(value, fallback?)` from `@sim/utils/errors` — extract error message string. Never write `e instanceof Error ? e.message : 'fallback'`
|
||||
- `structuredClone(value)` — built-in deep clone, no import needed. Never write `JSON.parse(JSON.stringify(obj))`
|
||||
- `omit(obj, keys)` from `@sim/utils/object` — remove keys from object
|
||||
- `filterUndefined(obj)` from `@sim/utils/object` — strip undefined-valued keys. Never write `Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined))`
|
||||
- `truncate(str, maxLength, suffix?)` from `@sim/utils/string` — safe string truncation with ellipsis
|
||||
- `backoffWithJitter(attempt, retryAfterMs, options?)` from `@sim/utils/retry` — exponential backoff with jitter
|
||||
- `parseRetryAfter(header)` from `@sim/utils/retry` — parse HTTP `Retry-After` header to milliseconds
|
||||
|
||||
```typescript
|
||||
// ✗ Bad
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
const msg = error instanceof Error ? error.message : String(error)
|
||||
const err = error instanceof Error ? error : new Error(String(error))
|
||||
const msg = error instanceof Error ? error.message : 'Unknown error'
|
||||
const clone = JSON.parse(JSON.stringify(obj))
|
||||
const filtered = Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined))
|
||||
|
||||
// ✓ Good
|
||||
import { sleep } from '@sim/utils/helpers'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { filterUndefined } from '@sim/utils/object'
|
||||
await sleep(1000)
|
||||
const msg = toError(error).message
|
||||
const err = toError(error)
|
||||
const msg = getErrorMessage(error, 'Unknown error')
|
||||
const clone = structuredClone(obj)
|
||||
const filtered = filterUndefined(obj)
|
||||
```
|
||||
|
||||
## Package Manager
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
description: SEO and GEO guidelines for the landing page
|
||||
globs: ["apps/sim/app/(home)/**/*.tsx"]
|
||||
---
|
||||
|
||||
# Landing Page — SEO / GEO
|
||||
|
||||
## SEO
|
||||
@@ -21,6 +20,6 @@ globs: ["apps/sim/app/(home)/**/*.tsx"]
|
||||
- **Answer-first pattern**: each section's H2 + subtitle should directly answer a user question (e.g. "What is Sim?", "How fast can I deploy?").
|
||||
- **Atomic answer blocks**: each feature / template card should be independently extractable by an AI summariser.
|
||||
- **Entity consistency**: always write "Sim" by name — never "the platform" or "our tool".
|
||||
- **Keyword density**: first 150 visible chars of Hero must name "Sim", "AI agents", "agentic workflows".
|
||||
- **Keyword density**: first 150 visible chars of Hero must name "Sim", "AI workspace", "AI agents".
|
||||
- **sr-only summaries**: Hero and Templates each have a `<p className="sr-only">` (~50 words) as an atomic product/catalog summary for AI citation.
|
||||
- **Specific numbers**: prefer concrete figures ("1,000+ integrations", "15+ AI providers") over vague claims.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
description: Core architecture principles for the Sim app
|
||||
globs: ["apps/sim/**"]
|
||||
---
|
||||
|
||||
# Sim App Architecture
|
||||
|
||||
## Core Principles
|
||||
@@ -14,19 +13,30 @@ globs: ["apps/sim/**"]
|
||||
## Root-Level Structure
|
||||
|
||||
```
|
||||
apps/sim/
|
||||
├── app/ # Next.js app router (pages, API routes)
|
||||
├── blocks/ # Block definitions and registry
|
||||
├── components/ # Shared UI (emcn/, ui/)
|
||||
├── executor/ # Workflow execution engine
|
||||
├── hooks/ # Shared hooks (queries/, selectors/)
|
||||
├── lib/ # App-wide utilities
|
||||
├── providers/ # LLM provider integrations
|
||||
├── stores/ # Zustand stores
|
||||
├── tools/ # Tool definitions
|
||||
└── triggers/ # Trigger definitions
|
||||
apps/
|
||||
├── sim/ # this app (Next.js: UI + API routes + workflow editor)
|
||||
│ ├── app/ # Next.js app router (pages, API routes)
|
||||
│ ├── blocks/ # Block definitions and registry
|
||||
│ ├── components/ # Shared UI (emcn/, ui/)
|
||||
│ ├── executor/ # Workflow execution engine
|
||||
│ ├── hooks/ # Shared hooks (queries/, selectors/)
|
||||
│ ├── lib/ # App-wide utilities
|
||||
│ ├── providers/ # LLM provider integrations
|
||||
│ ├── stores/ # Zustand stores
|
||||
│ ├── tools/ # Tool definitions
|
||||
│ └── triggers/ # Trigger definitions
|
||||
└── realtime/ # Bun Socket.IO server (collaborative canvas)
|
||||
|
||||
packages/ # @sim/* — audit, auth, db, logger, realtime-protocol,
|
||||
# security, tsconfig, utils, workflow-authz,
|
||||
# workflow-persistence, workflow-types
|
||||
```
|
||||
|
||||
## Package Boundaries
|
||||
|
||||
- `apps/* → packages/*` only. Packages never import from `apps/*`.
|
||||
- `apps/realtime` avoids Next.js, React, the block/tool registry, provider SDKs, and the executor; never add `@/lib/webhooks/providers/*`, `@/executor/*`, `@/blocks/*`, or `@/tools/*` imports to any package it consumes. CI enforces this via `scripts/check-monorepo-boundaries.ts` and `scripts/check-realtime-prune-graph.ts`.
|
||||
|
||||
## Feature Organization
|
||||
|
||||
Features live under `app/workspace/[workspaceId]/`:
|
||||
@@ -54,16 +64,3 @@ feature/
|
||||
- **Create `utils.ts` when** 2+ files need the same helper
|
||||
- **Check existing sources** before duplicating (`lib/` has many utilities)
|
||||
- **Location**: `lib/` (app-wide) → `feature/utils/` (feature-scoped) → inline (single-use)
|
||||
|
||||
## API Contracts
|
||||
|
||||
Boundary HTTP request and response shapes for all routes under `apps/sim/app/api/**` live in `apps/sim/lib/api/contracts/` (one file per resource family). Routes and clients consume the same contract — routes never define route-local boundary Zod schemas, and clients never define ad-hoc wire types. Domain validators that are not HTTP boundaries (tools, blocks, triggers, connectors, realtime handlers, internal helpers) may still use Zod directly; the contract rule is boundary-only.
|
||||
|
||||
- Each contract is built with `defineRouteContract({ method, path, params?, query?, body?, headers?, response: { mode: 'json', schema } })` and exports both schemas and named TypeScript type aliases (e.g., `export type CreateFolderBody = z.input<typeof createFolderBodySchema>`).
|
||||
- Shared identifier schemas live in `apps/sim/lib/api/contracts/primitives.ts`.
|
||||
- Routes validate via canonical helpers in `apps/sim/lib/api/server/validation.ts` (`parseRequest`, `validationErrorResponse`, `getValidationErrorMessage`, `isZodError`). Routes never `import { z } from 'zod'` and never use `instanceof z.ZodError`.
|
||||
- Clients call `requestJson(contract, ...)` from `apps/sim/lib/api/client/request.ts`; hooks import named type aliases from contracts, never `z.input/z.output`.
|
||||
- Routes under `apps/sim/app/api/v1/**` use `apps/sim/app/api/v1/middleware.ts` for shared auth, rate-limit, and workspace access. Compose contract validation inside that middleware.
|
||||
- `bun run check:api-validation` enforces this policy and must pass on PRs.
|
||||
|
||||
`bun run check:api-validation:strict` is the strict CI gate and additionally fails on annotations with empty reasons. Four per-line opt-out forms are recognized: `// boundary-raw-fetch: <reason>` (placed immediately above a legitimate raw `fetch(` call in `apps/sim/hooks/queries/**` or `apps/sim/hooks/selectors/**` for stream/binary/multipart/signed-URL/OAuth-redirect/external-origin cases), `// double-cast-allowed: <reason>` (placed immediately above an `as unknown as X` cast outside test files), `// boundary-raw-json: <reason>` (placed immediately above a raw `await request.json()` / `await req.json()` read in a route handler that cannot go through `parseRequest` — JSON-RPC envelopes, tolerant `.catch(() => ({}))` parses), and `// untyped-response: <reason>` (placed immediately above a `schema: z.unknown()` response declaration in a contract file when the response body is genuinely opaque). The reason must be non-empty. Whole-file allowlists for routes that legitimately import Zod for non-boundary reasons go through `INDIRECT_ZOD_ROUTES` in `scripts/check-api-validation-contracts.ts`, not per-line annotations.
|
||||
|
||||
@@ -2,47 +2,12 @@
|
||||
description: Component patterns and structure for React components
|
||||
globs: ["apps/sim/**/*.tsx"]
|
||||
---
|
||||
|
||||
# Component Patterns
|
||||
|
||||
## Structure Order
|
||||
Component authoring rules — structure order (refs → external hooks → store hooks → custom hooks → state → useMemo → useCallback → useEffect → return), required props interface, and extraction thresholds (50+ lines / 2+ files vs keep inline < 10 lines) — live in CLAUDE.md > Components.
|
||||
|
||||
```typescript
|
||||
'use client' // Only if using hooks
|
||||
`.tsx`-specific deltas not covered there:
|
||||
|
||||
// Imports (external → internal)
|
||||
// Constants at module level
|
||||
const CONFIG = { SPACING: 8 } as const
|
||||
|
||||
// Props interface
|
||||
interface ComponentProps {
|
||||
requiredProp: string
|
||||
optionalProp?: boolean
|
||||
}
|
||||
|
||||
export function Component({ requiredProp, optionalProp = false }: ComponentProps) {
|
||||
// a. Refs
|
||||
// b. External hooks (useParams, useRouter)
|
||||
// c. Store hooks
|
||||
// d. Custom hooks
|
||||
// e. Local state
|
||||
// f. useMemo
|
||||
// g. useCallback
|
||||
// h. useEffect
|
||||
// i. Return JSX
|
||||
}
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
1. `'use client'` only when using React hooks
|
||||
2. Always define props interface
|
||||
3. Extract constants with `as const`
|
||||
4. Semantic HTML (`aside`, `nav`, `article`)
|
||||
5. Optional chain callbacks: `onAction?.(id)`
|
||||
|
||||
## Component Extraction
|
||||
|
||||
**Extract when:** 50+ lines, used in 2+ files, or has own state/logic
|
||||
|
||||
**Keep inline when:** < 10 lines, single use, purely presentational
|
||||
- `'use client'` only when using React hooks or browser-only APIs.
|
||||
- Prefer semantic HTML (`aside`, `nav`, `article`).
|
||||
- Optional-chain callbacks: `onAction?.(id)`.
|
||||
|
||||
+15
-21
@@ -2,45 +2,39 @@
|
||||
description: Custom hook patterns and best practices
|
||||
globs: ["apps/sim/**/use-*.ts", "apps/sim/**/hooks/**/*.ts"]
|
||||
---
|
||||
|
||||
# Hook Patterns
|
||||
|
||||
## Structure
|
||||
|
||||
For server data, use a React Query hook from `hooks/queries/` — do NOT `useState` + `fetch` here (see `.claude/rules/sim-queries.md`). This pattern is for UI/orchestration hooks that hold UI-only state and wrap callbacks.
|
||||
|
||||
```typescript
|
||||
interface UseFeatureProps {
|
||||
id: string
|
||||
onSuccess?: (result: Result) => void
|
||||
onSelect?: (item: Item) => void
|
||||
}
|
||||
|
||||
export function useFeature({ id, onSuccess }: UseFeatureProps) {
|
||||
export function useFeature({ id, onSelect }: UseFeatureProps) {
|
||||
// 1. Refs for stable dependencies
|
||||
const idRef = useRef(id)
|
||||
const onSuccessRef = useRef(onSuccess)
|
||||
const onSelectRef = useRef(onSelect)
|
||||
|
||||
// 2. State
|
||||
const [data, setData] = useState<Data | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
// 2. UI-only state (never server data)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
// 3. Sync refs
|
||||
useEffect(() => {
|
||||
idRef.current = id
|
||||
onSuccessRef.current = onSuccess
|
||||
}, [id, onSuccess])
|
||||
onSelectRef.current = onSelect
|
||||
}, [id, onSelect])
|
||||
|
||||
// 4. Operations (useCallback with empty deps when using refs)
|
||||
const fetchData = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const result = await fetch(`/api/${idRef.current}`).then(r => r.json())
|
||||
setData(result)
|
||||
onSuccessRef.current?.(result)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
const select = useCallback((item: Item) => {
|
||||
onSelectRef.current?.(item)
|
||||
setIsOpen(false)
|
||||
}, [])
|
||||
|
||||
return { data, isLoading, fetchData }
|
||||
return { isOpen, setIsOpen, select }
|
||||
}
|
||||
```
|
||||
|
||||
@@ -50,5 +44,5 @@ export function useFeature({ id, onSuccess }: UseFeatureProps) {
|
||||
2. Props interface required
|
||||
3. Refs for stable callback dependencies
|
||||
4. Wrap returned functions in useCallback
|
||||
5. Always try/catch async operations
|
||||
6. Track loading/error states
|
||||
5. Server data goes through React Query (`hooks/queries/`), never `useState` + `fetch`
|
||||
6. Keep only UI/orchestration state in these hooks
|
||||
|
||||
@@ -2,284 +2,15 @@
|
||||
description: Adding new integrations (tools, blocks, triggers)
|
||||
globs: ["apps/sim/tools/**", "apps/sim/blocks/**", "apps/sim/triggers/**"]
|
||||
---
|
||||
|
||||
# Adding Integrations
|
||||
|
||||
## Overview
|
||||
Build order: **Tools** (`tools/{service}/`) → **Block** (`blocks/blocks/{service}.ts`) → **Icon** (`components/icons.tsx`) → optional **Trigger** (`triggers/{service}/`). Always look up the service's API docs first.
|
||||
|
||||
Adding a new integration typically requires:
|
||||
1. **Tools** - API operations (`tools/{service}/`)
|
||||
2. **Block** - UI component (`blocks/blocks/{service}.ts`)
|
||||
3. **Icon** - SVG icon (`components/icons.tsx`)
|
||||
4. **Trigger** (optional) - Webhooks/polling (`triggers/{service}/`)
|
||||
The full authoring instructions — tool/block/icon/trigger scaffolding, SubBlock property tables, `condition`/`required`/`dependsOn`/`mode`/`canonicalParamId` syntax, the `normalizeFileInput`/file-handling helper table, and checklists — live in the skills: `/add-integration` (end-to-end), `/add-tools`, `/add-block`, `/add-trigger`.
|
||||
|
||||
Always look up the service's API docs first.
|
||||
## Hard rules (don't get these wrong)
|
||||
|
||||
## 1. Tools (`tools/{service}/`)
|
||||
|
||||
```
|
||||
tools/{service}/
|
||||
├── index.ts # Export all tools
|
||||
├── types.ts # Params/response types
|
||||
├── {action}.ts # Individual tool (e.g., send_message.ts)
|
||||
└── ...
|
||||
```
|
||||
|
||||
**Tool file structure:**
|
||||
|
||||
```typescript
|
||||
// tools/{service}/{action}.ts
|
||||
import type { {Service}Params, {Service}Response } from '@/tools/{service}/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const {service}{Action}Tool: ToolConfig<{Service}Params, {Service}Response> = {
|
||||
id: '{service}_{action}',
|
||||
name: '{Service} {Action}',
|
||||
description: 'What this tool does',
|
||||
version: '1.0.0',
|
||||
oauth: { required: true, provider: '{service}' }, // if OAuth
|
||||
params: { /* param definitions */ },
|
||||
request: {
|
||||
url: '/api/tools/{service}/{action}',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({ ...params }),
|
||||
},
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!data.success) throw new Error(data.error)
|
||||
return { success: true, output: data.output }
|
||||
},
|
||||
outputs: { /* output definitions */ },
|
||||
}
|
||||
```
|
||||
|
||||
**Register in `tools/registry.ts`:**
|
||||
|
||||
```typescript
|
||||
import { {service}{Action}Tool } from '@/tools/{service}'
|
||||
// Add to registry object
|
||||
{service}_{action}: {service}{Action}Tool,
|
||||
```
|
||||
|
||||
## 2. Block (`blocks/blocks/{service}.ts`)
|
||||
|
||||
```typescript
|
||||
import { {Service}Icon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import type { {Service}Response } from '@/tools/{service}/types'
|
||||
|
||||
export const {Service}Block: BlockConfig<{Service}Response> = {
|
||||
type: '{service}',
|
||||
name: '{Service}',
|
||||
description: 'Short description',
|
||||
longDescription: 'Detailed description',
|
||||
category: 'tools',
|
||||
bgColor: '#hexcolor',
|
||||
icon: {Service}Icon,
|
||||
subBlocks: [ /* see SubBlock Properties below */ ],
|
||||
tools: {
|
||||
access: ['{service}_{action}', ...],
|
||||
config: {
|
||||
tool: (params) => `{service}_${params.operation}`,
|
||||
params: (params) => ({ ...params }),
|
||||
},
|
||||
},
|
||||
inputs: { /* input definitions */ },
|
||||
outputs: { /* output definitions */ },
|
||||
}
|
||||
```
|
||||
|
||||
### SubBlock Properties
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'fieldName', // Unique identifier
|
||||
title: 'Field Label', // UI label
|
||||
type: 'short-input', // See SubBlock Types below
|
||||
placeholder: 'Hint text',
|
||||
required: true, // See Required below
|
||||
condition: { ... }, // See Condition below
|
||||
dependsOn: ['otherField'], // See DependsOn below
|
||||
mode: 'basic', // 'basic' | 'advanced' | 'both' | 'trigger'
|
||||
}
|
||||
```
|
||||
|
||||
**SubBlock Types:** `short-input`, `long-input`, `dropdown`, `code`, `switch`, `slider`, `oauth-input`, `channel-selector`, `user-selector`, `file-upload`, etc.
|
||||
|
||||
### `condition` - Show/hide based on another field
|
||||
|
||||
```typescript
|
||||
// Show when operation === 'send'
|
||||
condition: { field: 'operation', value: 'send' }
|
||||
|
||||
// Show when operation is 'send' OR 'read'
|
||||
condition: { field: 'operation', value: ['send', 'read'] }
|
||||
|
||||
// Show when operation !== 'send'
|
||||
condition: { field: 'operation', value: 'send', not: true }
|
||||
|
||||
// Complex: NOT in list AND another condition
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_channels', 'list_users'],
|
||||
not: true,
|
||||
and: { field: 'destinationType', value: 'dm', not: true }
|
||||
}
|
||||
```
|
||||
|
||||
### `required` - Field validation
|
||||
|
||||
```typescript
|
||||
// Always required
|
||||
required: true
|
||||
|
||||
// Conditionally required (same syntax as condition)
|
||||
required: { field: 'operation', value: 'send' }
|
||||
```
|
||||
|
||||
### `dependsOn` - Clear field when dependencies change
|
||||
|
||||
```typescript
|
||||
// Clear when credential changes
|
||||
dependsOn: ['credential']
|
||||
|
||||
// Clear when authMethod changes AND (credential OR botToken) changes
|
||||
dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] }
|
||||
```
|
||||
|
||||
### `mode` - When to show field
|
||||
|
||||
- `'basic'` - Only in basic mode (default UI)
|
||||
- `'advanced'` - Only in advanced mode (manual input)
|
||||
- `'both'` - Show in both modes (default)
|
||||
- `'trigger'` - Only when block is used as trigger
|
||||
|
||||
### `canonicalParamId` - Link basic/advanced alternatives
|
||||
|
||||
Use to map multiple UI inputs to a single logical parameter:
|
||||
|
||||
```typescript
|
||||
// Basic mode: Visual selector
|
||||
{
|
||||
id: 'fileSelector',
|
||||
type: 'file-selector',
|
||||
mode: 'basic',
|
||||
canonicalParamId: 'fileId',
|
||||
required: true,
|
||||
},
|
||||
// Advanced mode: Manual input
|
||||
{
|
||||
id: 'manualFileId',
|
||||
type: 'short-input',
|
||||
mode: 'advanced',
|
||||
canonicalParamId: 'fileId',
|
||||
required: true,
|
||||
},
|
||||
```
|
||||
|
||||
**Critical Rules:**
|
||||
- `canonicalParamId` must NOT match any subblock's `id`
|
||||
- `canonicalParamId` must be unique per operation/condition context
|
||||
- **Required consistency:** All subblocks in a canonical group must have the same `required` status
|
||||
- **Inputs section:** Must list canonical param IDs (e.g., `fileId`), NOT raw subblock IDs
|
||||
- **Params function:** Must use canonical param IDs (raw IDs are deleted after canonical transformation)
|
||||
|
||||
**Register in `blocks/registry.ts`:**
|
||||
|
||||
```typescript
|
||||
import { {Service}Block } from '@/blocks/blocks/{service}'
|
||||
// Add to registry object (alphabetically)
|
||||
{service}: {Service}Block,
|
||||
```
|
||||
|
||||
## 3. Icon (`components/icons.tsx`)
|
||||
|
||||
```typescript
|
||||
export function {Service}Icon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* SVG path from service's brand assets */}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Trigger (`triggers/{service}/`) - Optional
|
||||
|
||||
```
|
||||
triggers/{service}/
|
||||
├── index.ts # Export all triggers
|
||||
├── webhook.ts # Webhook handler
|
||||
├── utils.ts # Shared utilities
|
||||
└── {event}.ts # Specific event handlers
|
||||
```
|
||||
|
||||
**Register in `triggers/registry.ts`:**
|
||||
|
||||
```typescript
|
||||
import { {service}WebhookTrigger } from '@/triggers/{service}'
|
||||
// Add to TRIGGER_REGISTRY
|
||||
{service}_webhook: {service}WebhookTrigger,
|
||||
```
|
||||
|
||||
## File Handling
|
||||
|
||||
When integrations handle file uploads/downloads, use `UserFile` objects consistently.
|
||||
|
||||
### File Input (Uploads)
|
||||
|
||||
1. **Block subBlocks:** Use basic/advanced mode pattern with `canonicalParamId`
|
||||
2. **Normalize in block config:** Use `normalizeFileInput` from `@/blocks/utils`
|
||||
3. **Internal API route:** Create route that uses `downloadFileFromStorage` to get file content
|
||||
4. **Tool routes to internal API:** Don't call external APIs directly with files
|
||||
|
||||
```typescript
|
||||
// In block tools.config:
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
|
||||
const normalizedFile = normalizeFileInput(
|
||||
params.uploadFile || params.fileRef || params.fileContent,
|
||||
{ single: true }
|
||||
)
|
||||
if (normalizedFile) params.file = normalizedFile
|
||||
```
|
||||
|
||||
### File Output (Downloads)
|
||||
|
||||
Use `FileToolProcessor` in tool `transformResponse` to store files:
|
||||
|
||||
```typescript
|
||||
import { FileToolProcessor } from '@/executor/utils/file-tool-processor'
|
||||
|
||||
const processor = new FileToolProcessor(context)
|
||||
const file = await processor.processFileData({
|
||||
data: base64Content,
|
||||
mimeType: 'application/pdf',
|
||||
filename: 'doc.pdf',
|
||||
})
|
||||
```
|
||||
|
||||
### Key Helpers
|
||||
|
||||
| Helper | Location | Purpose |
|
||||
|--------|----------|---------|
|
||||
| `normalizeFileInput` | `@/blocks/utils` | Normalize file params in block config |
|
||||
| `processFilesToUserFiles` | `@/lib/uploads/utils/file-utils` | Convert raw inputs to UserFile[] |
|
||||
| `downloadFileFromStorage` | `@/lib/uploads/utils/file-utils.server` | Get Buffer from UserFile |
|
||||
| `FileToolProcessor` | `@/executor/utils/file-tool-processor` | Process tool output files |
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Look up API docs for the service
|
||||
- [ ] Create `tools/{service}/types.ts` with proper types
|
||||
- [ ] Create tool files for each operation
|
||||
- [ ] Create `tools/{service}/index.ts` barrel export
|
||||
- [ ] Register tools in `tools/registry.ts`
|
||||
- [ ] Add icon to `components/icons.tsx`
|
||||
- [ ] Create block in `blocks/blocks/{service}.ts`
|
||||
- [ ] Register block in `blocks/registry.ts`
|
||||
- [ ] (Optional) Create triggers in `triggers/{service}/`
|
||||
- [ ] (Optional) Register triggers in `triggers/registry.ts`
|
||||
- [ ] (If file uploads) Create internal API route with `downloadFileFromStorage`
|
||||
- [ ] (If file uploads) Use `normalizeFileInput` in block config
|
||||
- Tool IDs are `snake_case` (`service_action`). Register tools in `tools/registry.ts`, blocks in `blocks/registry.ts` (alphabetically), triggers in `triggers/registry.ts`.
|
||||
- Type coercions (`Number()`, etc.) belong in `tools.config.params` (runs at execution, after variable resolution) — never in `tools.config.tool` (runs at serialization; coercing there destroys dynamic `<Block.output>` references).
|
||||
- `canonicalParamId` must NOT match any subblock's `id`, must be unique per operation/condition context, and all subblocks in a canonical group must share the same `required` status. The `inputs` section and the params function reference canonical IDs, not raw subblock IDs.
|
||||
- Blocks must also set the catalog/UI metadata fields `integrationType`, `tags`, `authMode`, `docsLink`, and export a `{Service}BlockMeta` — see the `/add-block` skill's BlockMeta section for details.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
description: React Query patterns for the Sim application
|
||||
globs: ["apps/sim/hooks/queries/**/*.ts"]
|
||||
---
|
||||
|
||||
# React Query Patterns
|
||||
|
||||
All React Query hooks live in `hooks/queries/`. All server state must go through React Query — never use `useState` + `fetch` in components for data fetching or mutations.
|
||||
@@ -57,7 +56,7 @@ export function useEntityList(workspaceId?: string, options?: { enabled?: boolea
|
||||
queryFn: ({ signal }) => fetchEntities(workspaceId as string, signal),
|
||||
enabled: Boolean(workspaceId) && (options?.enabled ?? true),
|
||||
staleTime: 60 * 1000,
|
||||
placeholderData: keepPreviousData,
|
||||
placeholderData: keepPreviousData, // OK: workspaceId varies
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -66,12 +65,14 @@ export function useEntityList(workspaceId?: string, options?: { enabled?: boolea
|
||||
|
||||
- Use targeted invalidation (`entityKeys.lists()`) not broad (`entityKeys.all`) when possible
|
||||
- Invalidation must cover all affected query key prefixes (lists, details, related views)
|
||||
- Use `onSuccess` invalidation for plain mutations; use `onSettled` for optimistic mutations so the cache is reconciled on both success and error (see Optimistic Updates below)
|
||||
- `mutationFn` calls go through `requestJson(contract, { body, signal })` from `@/lib/api/client/request` — same boundary rule as queries
|
||||
|
||||
```typescript
|
||||
export function useCreateEntity() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (variables) => { /* fetch POST */ },
|
||||
mutationFn: (body: CreateEntityBody) => requestJson(createEntityContract, { body }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: entityKeys.lists() })
|
||||
},
|
||||
@@ -126,9 +127,8 @@ const handler = useCallback(() => {
|
||||
|
||||
## Boundary Types
|
||||
|
||||
- Hooks must import named type aliases from `@/lib/api/contracts/**` (e.g., `import { listEntitiesContract, type EntityList } from '@/lib/api/contracts/entities'`). Never write `z.input<...>` or `z.output<...>` in hooks.
|
||||
- Hooks must not `import { z } from 'zod'`. Boundary types come from contract aliases; non-boundary helpers can stay in plain TypeScript.
|
||||
- For non-contract endpoints (multipart uploads, binary downloads, streaming responses, signed-URL flows, OAuth redirects, external origins), it is OK to keep raw `fetch`. Each legitimate raw `fetch(` call inside `apps/sim/hooks/queries/**` or `apps/sim/hooks/selectors/**` must be preceded by a `// boundary-raw-fetch: <reason>` annotation on the immediately preceding line (up to three non-empty preceding comment lines are tolerated). The reason must be non-empty — empty reasons fail strict mode. The audit script `scripts/check-api-validation-contracts.ts` (`bun run check:api-validation` / `bun run check:api-validation:strict`) enforces this.
|
||||
- Hooks import named type aliases from `@/lib/api/contracts/**` (e.g., `import { listEntitiesContract, type EntityList } from '@/lib/api/contracts/entities'`). Never write `z.input<...>` / `z.output<...>` in hooks, and never `import { z } from 'zod'` in client code.
|
||||
- Raw `fetch` is allowed only for documented exceptions — multipart uploads, binary downloads, streaming responses, signed-URL flows, OAuth redirects, external origins. Each such raw `fetch(` inside `apps/sim/hooks/queries/**` or `apps/sim/hooks/selectors/**` — and any same-origin `/api/...` fetch elsewhere under `apps/sim/**` outside an API route handler — must be preceded by a `// boundary-raw-fetch: <reason>` annotation (reason non-empty; up to three preceding comment lines tolerated). Enforced by `scripts/check-api-validation-contracts.ts` (`bun run check:api-validation` / `:strict`).
|
||||
|
||||
## Naming
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
description: Tailwind CSS and styling conventions
|
||||
globs: ["apps/sim/**/*.tsx", "apps/sim/**/*.css"]
|
||||
---
|
||||
|
||||
# Styling Rules
|
||||
|
||||
## Tailwind
|
||||
@@ -15,7 +14,7 @@ globs: ["apps/sim/**/*.tsx", "apps/sim/**/*.css"]
|
||||
## Conditional Classes
|
||||
|
||||
```typescript
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
|
||||
<div className={cn(
|
||||
'base-classes',
|
||||
@@ -38,3 +37,38 @@ setWidth: (width) => {
|
||||
// In component
|
||||
<aside style={{ width: 'var(--sidebar-width)' }} />
|
||||
```
|
||||
|
||||
## Text Scale
|
||||
|
||||
Custom font sizes (`apps/sim/tailwind.config.ts`): `text-micro`=10px, `text-xs`=11px, `text-caption`=12px, `text-small`=13px, `text-base`=15px. `text-sm` is Tailwind default 14px. Field titles use `text-small` (13px); hints/errors use `text-caption` (12px).
|
||||
|
||||
Icons default `size-[14px]`. Equal h/w → `size-*` (`size-[14px]`, `size-4`), never `h-N w-N`.
|
||||
|
||||
## Color Tokens
|
||||
|
||||
Value text `--text-body`; muted/placeholder/labels `--text-muted`; icons `--text-icon`; borders `--border-1` (fields) / `--border` (dividers); surfaces `--surface-5` (light) / `--surface-4` (dark); active row `--surface-active`; error `--text-error`. No focus rings on chip surfaces.
|
||||
|
||||
## Chip Components (consumer usage)
|
||||
|
||||
`ChipInput`, `ChipTextarea`, `ChipModal*` own their full chrome. Consumers describe intent through PROPS; they never re-style the chrome. The canonical chrome lives in `apps/sim/components/emcn/components/chip-input/chip-field-chrome.ts` — never hand-roll `rounded-lg`/`border`/`bg-[var(--surface-5)]`/`h-[30px]`/`px-2`/`text-sm`/focus rings.
|
||||
|
||||
### Props over className
|
||||
|
||||
- **Errors** → `error` prop. Never `className={cn(err && 'border-[var(--text-error)]')}`.
|
||||
- **Leading icon** → `icon` prop (rendered 14px in `--text-icon`).
|
||||
- **Trailing buttons** (reveal/copy/fetch) → `endAdornment`.
|
||||
- **Inner-input styling** (e.g. `font-mono`, number-spinner reset) → `inputClassName` (ChipInput only). See `app/workspace/[workspaceId]/settings/components/billing/components/usage-limit-field/usage-limit-field.tsx:117`.
|
||||
- **`ChipModalField` controls take NO className.** Pass `title`/`value`/`onChange`/`error`/`hint`/`required`/`flush`. The field owns label, control, and error/hint rendering. See `app/workspace/[workspaceId]/skills/components/skill-modal/skill-modal.tsx:170`.
|
||||
|
||||
### What className MAY carry
|
||||
|
||||
Layout/sizing ONLY: `flex-1`, `w-full`, `w-[Npx]`, `min-w-0`, `max-w-*`, margins, `truncate`. Example: `<ChipInput icon={Search} className='min-w-0 flex-1' .../>` (`app/workspace/[workspaceId]/integrations/integrations.tsx:257`). NEVER re-specify canonical chrome — the component already applies it.
|
||||
|
||||
### Form / chip-modal layout rhythm
|
||||
|
||||
- **Field row** = `ChipModalField`: label↔control `gap-[9px]`, field gutter `px-2` (`px-0` when `flush`). Title = `Label` at `text-small` (13px), muted, normal weight; hint/error at `text-caption` (12px).
|
||||
- **Modal body** (`ChipModalBody`): `gap-4` between fields, padding `px-2 pt-4 pb-4.5`.
|
||||
- **Header/footer**: horizontal gutter `px-4` (header `pt-3`; footer `px-4 pt-2 pb-2`, tinted bar).
|
||||
- **Page section rhythm** (integrations/skills/settings): muted `text-small` label + `mt-[9px] mb-3 h-px bg-[var(--border)]` divider, sections stacked `gap-7`. Reuse `SettingsSection` (`app/workspace/[workspaceId]/settings/components/settings-section/settings-section.tsx`) rather than re-deriving it.
|
||||
|
||||
When a standalone labeled field outside a `ChipModal` needs the same look (e.g. `SkillImport`), match the field rhythm by hand: `flex flex-col gap-[9px]`, muted label, `ChipInput`/`ChipTextarea` control, `text-caption` error below.
|
||||
|
||||
@@ -22,6 +22,7 @@ These modules are mocked globally — do NOT re-mock them in test files unless y
|
||||
- `@/stores/console/store`, `@/stores/terminal`, `@/stores/execution/store`
|
||||
- `@/blocks/registry`
|
||||
- `@trigger.dev/sdk`
|
||||
- `@sim/workflow-authz` → `workflowAuthzMock`
|
||||
|
||||
## Structure
|
||||
|
||||
@@ -196,24 +197,38 @@ hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
|
||||
|
||||
### Database chain mocking
|
||||
|
||||
```typescript
|
||||
const { mockSelect, mockFrom, mockWhere } = vi.hoisted(() => ({
|
||||
mockSelect: vi.fn(),
|
||||
mockFrom: vi.fn(),
|
||||
mockWhere: vi.fn(),
|
||||
}))
|
||||
Use the centralized `dbChainMock` + `dbChainMockFns` helpers — no `vi.hoisted()` or chain-wiring boilerplate needed.
|
||||
|
||||
vi.mock('@sim/db', () => ({
|
||||
db: { select: mockSelect },
|
||||
}))
|
||||
```typescript
|
||||
import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing'
|
||||
|
||||
vi.mock('@sim/db', () => dbChainMock)
|
||||
// Spread for custom exports: vi.mock('@sim/db', () => ({ ...dbChainMock, myTable: {...} }))
|
||||
|
||||
beforeEach(() => {
|
||||
mockSelect.mockReturnValue({ from: mockFrom })
|
||||
mockFrom.mockReturnValue({ where: mockWhere })
|
||||
mockWhere.mockResolvedValue([{ id: '1', name: 'test' }])
|
||||
vi.clearAllMocks()
|
||||
resetDbChainMock() // only needed if tests use permanent (non-`Once`) overrides
|
||||
})
|
||||
|
||||
it('reads a row', async () => {
|
||||
dbChainMockFns.limit.mockResolvedValueOnce([{ id: '1', name: 'test' }])
|
||||
// exercise code that hits db.select().from().where().limit()
|
||||
expect(dbChainMockFns.where).toHaveBeenCalled()
|
||||
})
|
||||
```
|
||||
|
||||
**Default chains supported:**
|
||||
- `select()/selectDistinct()/selectDistinctOn() → from() → where()/innerJoin()/leftJoin() → where() → limit()/orderBy()/returning()/groupBy()/for()`
|
||||
- `insert() → values() → returning()/onConflictDoUpdate()/onConflictDoNothing()`
|
||||
- `update() → set() → where() → limit()/orderBy()/returning()/for()`
|
||||
- `delete() → where() → limit()/orderBy()/returning()/for()`
|
||||
- `db.execute()` resolves `[]`
|
||||
- `db.transaction(cb)` calls cb with `dbChainMock.db`
|
||||
|
||||
`.for('update')` (Postgres row-level locking) is supported on `where` builders. It returns a thenable with `.limit` / `.orderBy` / `.returning` / `.groupBy` attached, so both `await .where().for('update')` (terminal) and `await .where().for('update').limit(1)` (chained) work. Override the terminal result with `dbChainMockFns.for.mockResolvedValueOnce([...])`; for the chained form, mock the downstream terminal (e.g. `dbChainMockFns.limit.mockResolvedValueOnce([...])`).
|
||||
|
||||
All terminals default to `Promise.resolve([])`. Override per-test with `dbChainMockFns.<terminal>.mockResolvedValueOnce(...)`. Use `resetDbChainMock()` in `beforeEach` only when tests replace wiring with `.mockReturnValue` / `.mockResolvedValue` (permanent); tests using only `...Once` variants don't need it.
|
||||
|
||||
## @sim/testing Package
|
||||
|
||||
Always prefer over local test data.
|
||||
|
||||
@@ -5,10 +5,19 @@ You are a professional software engineer. All code must follow best practices: a
|
||||
## Global Standards
|
||||
|
||||
- **Linting / Audit**: `bun run check:api-validation` must pass on PRs. Do not introduce route-local boundary Zod schemas, direct route Zod imports, or ad-hoc client wire types — see "API Contracts" and "API Route Pattern" below
|
||||
- **Logging**: Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`
|
||||
- **Logging**: Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`. Inside API routes wrapped with `withRouteHandler`, loggers automatically include the request ID — no manual `withMetadata({ requestId })` needed
|
||||
- **API Route Handlers**: All API route handlers (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`) must be wrapped with `withRouteHandler` from `@/lib/core/utils/with-route-handler`. This provides request ID tracking, automatic error logging for 4xx/5xx responses, and unhandled error catching. See "API Route Pattern" section below
|
||||
- **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments
|
||||
- **Styling**: Never update global styles. Keep all styling local to components
|
||||
- **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@sim/utils/id`
|
||||
- **Common Utilities**: Use shared helpers from `@sim/utils` instead of inline implementations:
|
||||
- `sleep(ms)` from `@sim/utils/helpers` — never `new Promise(resolve => setTimeout(resolve, ms))`
|
||||
- `toError(e)` from `@sim/utils/errors` — normalize caught values to `Error`
|
||||
- `getErrorMessage(e, fallback?)` from `@sim/utils/errors` — extract message string from unknown caught value; never write `e instanceof Error ? e.message : 'fallback'`
|
||||
- `structuredClone(value)` — built-in deep clone; never `JSON.parse(JSON.stringify(...))`
|
||||
- `omit(obj, keys)` / `filterUndefined(obj)` from `@sim/utils/object` — object trimming; never `Object.fromEntries(Object.entries(...).filter(...))`
|
||||
- `truncate(str, maxLength, suffix?)` from `@sim/utils/string` — never inline slice + ellipsis
|
||||
- `backoffWithJitter(attempt, retryAfterMs, options?)` / `parseRetryAfter(header)` from `@sim/utils/retry` — shared retry pacing; never reimplement exponential backoff inline
|
||||
- **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx`
|
||||
|
||||
## Architecture
|
||||
@@ -137,7 +146,7 @@ Domain validators that are not HTTP boundaries — tools, blocks, triggers, conn
|
||||
|
||||
A small number of legitimate exceptions to the boundary rules are tolerated when annotated. The audit script recognizes four annotation forms:
|
||||
|
||||
- `// boundary-raw-fetch: <reason>` — placed on the line directly above a raw `fetch(` call inside `apps/sim/hooks/queries/**` or `apps/sim/hooks/selectors/**`. Use only for documented exceptions: streaming responses, binary downloads, multipart uploads, signed-URL flows, OAuth redirects, and external-origin requests
|
||||
- `// boundary-raw-fetch: <reason>` — placed on the line directly above a raw `fetch(` call inside `apps/sim/hooks/queries/**`, `apps/sim/hooks/selectors/**`, or any other client/UI source under `apps/sim/**` that targets a same-origin `/api/...` URL outside an API route handler. Use only for documented exceptions: streaming responses, binary downloads, multipart uploads, signed-URL flows, OAuth redirects, and external-origin requests
|
||||
- `// double-cast-allowed: <reason>` — placed on the line directly above an `as unknown as X` cast outside test files
|
||||
- `// boundary-raw-json: <reason>` — placed on the line directly above a raw `await request.json()` / `await req.json()` read in a route handler. Use only when the body is a JSON-RPC envelope, a tolerant `.catch(() => ({}))` parse, or otherwise cannot go through `parseRequest`
|
||||
- `// untyped-response: <reason>` — placed on the line directly above a `schema: z.unknown()` response declaration in a contract file. Use only when the response body is genuinely opaque (user-supplied data, third-party passthrough)
|
||||
@@ -160,6 +169,8 @@ const provider = config as unknown as LegacyProvider
|
||||
|
||||
## API Route Pattern
|
||||
|
||||
Every API route handler must be wrapped with `withRouteHandler`. This sets up `AsyncLocalStorage`-based request context so all loggers in the request lifecycle automatically include the request ID. Never export a bare `async function GET/POST/...` — always use `export const METHOD = withRouteHandler(...)`.
|
||||
|
||||
Routes never `import { z } from 'zod'` and never define route-local boundary schemas. They consume the contract from `@/lib/api/contracts/**` and validate with canonical helpers from `@/lib/api/server`:
|
||||
|
||||
- `parseRequest(contract, request, context, options?)` — fully contract-bound routes; parses params, query, body, and headers in one call. Pass `{}` for `context` on routes without route params, or the route's `context` argument when route params exist. Returns a discriminated union; check `parsed.success` and return `parsed.response` on failure
|
||||
@@ -188,6 +199,14 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
})
|
||||
```
|
||||
|
||||
### Composing with other middleware
|
||||
|
||||
```typescript
|
||||
export const POST = withRouteHandler(withAdminAuth(async (request) => {
|
||||
return NextResponse.json({ ok: true })
|
||||
}))
|
||||
```
|
||||
|
||||
Routes under `apps/sim/app/api/v1/**` use the shared middleware in `apps/sim/app/api/v1/middleware.ts` for auth, rate-limit, and workspace access. Compose contract validation inside that middleware — never reimplement auth/rate-limit per-route.
|
||||
|
||||
### Adding a new boundary feature end-to-end
|
||||
@@ -351,7 +370,7 @@ export function useUpdateEntity() {
|
||||
|
||||
## Styling
|
||||
|
||||
Use Tailwind only, no inline styles. Use `cn()` from `@/lib/utils` for conditional classes.
|
||||
Use Tailwind only, no inline styles. Use `cn()` from `@/lib/core/utils/cn` for conditional classes.
|
||||
|
||||
```typescript
|
||||
<div className={cn('base-classes', isActive && 'active-classes')} />
|
||||
@@ -363,9 +382,25 @@ For equal height and width, use the `size-*` shorthand — never `h-[Npx] w-[Npx
|
||||
<Icon className='size-[14px] text-[var(--text-icon)]' />
|
||||
```
|
||||
|
||||
On chip components (see "EMCN Components"), drive chrome through PROPS, not `className`: `error` for the error state, `icon`/`endAdornment` for adornments, `inputClassName` for the inner field. `className` carries ONLY layout/sizing — never re-specify canonical chrome (border, fill, radius, height, text/icon color) or add focus rings. Full consumer rules in `.claude/rules/sim-styling.md`.
|
||||
|
||||
## EMCN Components
|
||||
|
||||
Import from `@/components/emcn`, never from subpaths (except CSS files). Use CVA when 2+ variants exist.
|
||||
Import from `@/components/emcn`, never from subpaths (except CSS files). Use CVA only when 2+ genuine variants exist; otherwise plain `cn()`.
|
||||
|
||||
The chip pill is the canonical UI chrome and is progressively replacing the legacy EMCN primitives — prefer the chip equivalents (`ChipInput`/`ChipTextarea` over `Input`/`Textarea`, plus `Chip`/`ChipLink`, `ChipDropdown`, `ChipSelect`/`ChipCombobox`, `ChipModal`, `ChipSwitch`, `ChipTag`, `ChipDatePicker`). Components OWN their chrome (single source of truth) — consumers pass props, not class overrides. Authoring rules in `.claude/rules/emcn-components.md`; consumer rules in `.claude/rules/sim-styling.md`.
|
||||
|
||||
## Design-System Consolidation
|
||||
|
||||
Principles when building or migrating shared UI:
|
||||
|
||||
- One canonical source of truth for shared chrome — compose it, never re-derive it per consumer.
|
||||
- Props-driven API over `className` overrides — reaching for `className` to change chrome is a smell; expose a prop instead.
|
||||
- Discriminated-union props for modes (e.g. `ChipDropdown multiple`) over near-duplicate components.
|
||||
- Delete legacy variants/components after migration — no parallel paths left behind.
|
||||
- Plain `cn()` for a single error/state toggle; CVA only for genuinely multiple variants.
|
||||
- Align consumers to the canonical defaults — normal weight, `--text-body` text, `--text-icon` icons.
|
||||
- Verify referenced CSS vars exist — an undefined var silently falls back to `currentColor` (black-bug).
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
@@ -32,19 +32,40 @@ You are a professional software engineer. All code must follow best practices: a
|
||||
### Root Structure
|
||||
|
||||
```
|
||||
apps/sim/
|
||||
├── app/ # Next.js app router (pages, API routes)
|
||||
├── blocks/ # Block definitions and registry
|
||||
├── components/ # Shared UI (emcn/, ui/)
|
||||
├── executor/ # Workflow execution engine
|
||||
├── hooks/ # Shared hooks (queries/, selectors/)
|
||||
├── lib/ # App-wide utilities
|
||||
├── providers/ # LLM provider integrations
|
||||
├── stores/ # Zustand stores
|
||||
├── tools/ # Tool definitions
|
||||
└── triggers/ # Trigger definitions
|
||||
apps/
|
||||
├── sim/ # Next.js app (UI + API routes + workflow editor)
|
||||
│ ├── app/ # Next.js app router (pages, API routes)
|
||||
│ ├── blocks/ # Block definitions and registry
|
||||
│ ├── components/ # Shared UI (emcn/, ui/)
|
||||
│ ├── executor/ # Workflow execution engine
|
||||
│ ├── hooks/ # Shared hooks (queries/, selectors/)
|
||||
│ ├── lib/ # App-wide utilities
|
||||
│ ├── providers/ # LLM provider integrations
|
||||
│ ├── stores/ # Zustand stores
|
||||
│ ├── tools/ # Tool definitions
|
||||
│ └── triggers/ # Trigger definitions
|
||||
└── realtime/ # Bun Socket.IO server (collaborative canvas)
|
||||
|
||||
packages/
|
||||
├── audit/ # @sim/audit
|
||||
├── auth/ # @sim/auth — shared Better Auth verifier
|
||||
├── db/ # @sim/db — drizzle schema + client
|
||||
├── logger/ # @sim/logger
|
||||
├── realtime-protocol/ # @sim/realtime-protocol — socket op constants + zod schemas
|
||||
├── security/ # @sim/security — safeCompare
|
||||
├── tsconfig/ # shared tsconfig presets
|
||||
├── utils/ # @sim/utils
|
||||
├── workflow-authz/ # @sim/workflow-authz
|
||||
├── workflow-persistence/ # @sim/workflow-persistence
|
||||
└── workflow-types/ # @sim/workflow-types — pure BlockState/Loop/Parallel types
|
||||
```
|
||||
|
||||
### Package boundaries
|
||||
|
||||
- `apps/* → packages/*` only. Packages never import from `apps/*`.
|
||||
- `apps/realtime` intentionally avoids Next.js, React, the block/tool registry, provider SDKs, and the executor. Do not add imports from `@/lib/webhooks/providers/*`, `@/executor/*`, `@/blocks/*`, or `@/tools/*` to any package consumed by `apps/realtime`. CI enforces this via `scripts/check-monorepo-boundaries.ts` and `scripts/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`).
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- Components: PascalCase (`WorkflowList`)
|
||||
@@ -122,7 +143,7 @@ Domain validators that are not HTTP boundaries — tools, blocks, triggers, conn
|
||||
|
||||
A small number of legitimate exceptions to the boundary rules are tolerated when annotated. The audit script recognizes four annotation forms:
|
||||
|
||||
- `// boundary-raw-fetch: <reason>` — placed on the line directly above a raw `fetch(` call inside `apps/sim/hooks/queries/**` or `apps/sim/hooks/selectors/**`. Use only for documented exceptions: streaming responses, binary downloads, multipart uploads, signed-URL flows, OAuth redirects, and external-origin requests
|
||||
- `// boundary-raw-fetch: <reason>` — placed on the line directly above a raw `fetch(` call in client hooks (`apps/sim/hooks/queries/**`, `apps/sim/hooks/selectors/**`) AND any same-origin `/api/...` fetch elsewhere under `apps/sim/**` outside an API route handler. Use only for documented exceptions: streaming responses, binary downloads, multipart uploads, signed-URL flows, OAuth redirects, and external-origin requests
|
||||
- `// double-cast-allowed: <reason>` — placed on the line directly above an `as unknown as X` cast outside test files
|
||||
- `// boundary-raw-json: <reason>` — placed on the line directly above a raw `await request.json()` / `await req.json()` read in a route handler. Use only when the body is a JSON-RPC envelope, a tolerant `.catch(() => ({}))` parse, or otherwise cannot go through `parseRequest`
|
||||
- `// untyped-response: <reason>` — placed on the line directly above a `schema: z.unknown()` response declaration in a contract file. Use only when the response body is genuinely opaque (user-supplied data, third-party passthrough)
|
||||
@@ -260,7 +281,7 @@ Hooks consume contracts the same way routes do. Every same-origin JSON call must
|
||||
|
||||
- Hooks import named type aliases from `@/lib/api/contracts/**`. Never write `z.input<...>` / `z.output<...>` in hooks, and never `import { z } from 'zod'` in client code
|
||||
- `requestJson` parses params, query, body, and headers against the contract on the way out and validates the JSON response on the way back. Hooks always forward `signal` for cancellation
|
||||
- Documented exceptions for raw `fetch`: streaming responses, binary downloads, multipart uploads, signed-URL flows, OAuth redirects, and external-origin requests. Mark each raw `fetch` with a TSDoc comment explaining which exception applies
|
||||
- Documented exceptions for raw `fetch`: streaming responses, binary downloads, multipart uploads, signed-URL flows, OAuth redirects, and external-origin requests. Mark each raw `fetch` with a TSDoc comment explaining which exception applies. The `// boundary-raw-fetch` annotation is required not only in client hooks but for any same-origin `/api/...` fetch anywhere under `apps/sim/**` outside an API route handler — strict CI flags these regardless of location
|
||||
|
||||
```typescript
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query'
|
||||
@@ -348,7 +369,7 @@ export function useUpdateEntity() {
|
||||
|
||||
## Styling
|
||||
|
||||
Use Tailwind only, no inline styles. Use `cn()` from `@/lib/utils` for conditional classes.
|
||||
Use Tailwind only, no inline styles. Use `cn()` from `@/lib/core/utils/cn` for conditional classes.
|
||||
|
||||
```typescript
|
||||
<div className={cn('base-classes', isActive && 'active-classes')} />
|
||||
@@ -360,9 +381,25 @@ For equal height and width, use the `size-*` shorthand — never `h-[Npx] w-[Npx
|
||||
<Icon className='size-[14px] text-[var(--text-icon)]' />
|
||||
```
|
||||
|
||||
On chip components (see "EMCN Components"), drive chrome through PROPS, not `className`: `error` for the error state, `icon`/`endAdornment` for adornments, `inputClassName` for the inner field. `className` carries ONLY layout/sizing — never re-specify canonical chrome (border, fill, radius, height, text/icon color) or add focus rings. Full consumer rules in `.claude/rules/sim-styling.md`.
|
||||
|
||||
## EMCN Components
|
||||
|
||||
Import from `@/components/emcn`, never from subpaths (except CSS files). Use CVA when 2+ variants exist.
|
||||
Import from `@/components/emcn`, never from subpaths (except CSS files). Use CVA only when 2+ genuine variants exist; otherwise plain `cn()`.
|
||||
|
||||
The chip pill is the canonical UI chrome and is progressively replacing the legacy EMCN primitives — prefer the chip equivalents (`ChipInput`/`ChipTextarea` over `Input`/`Textarea`, plus `Chip`/`ChipLink`, `ChipDropdown`, `ChipSelect`/`ChipCombobox`, `ChipModal`, `ChipSwitch`, `ChipTag`, `ChipDatePicker`). Components OWN their chrome (single source of truth) — consumers pass props, not class overrides. Authoring rules in `.claude/rules/emcn-components.md`; consumer rules in `.claude/rules/sim-styling.md`.
|
||||
|
||||
## Design-System Consolidation
|
||||
|
||||
Principles when building or migrating shared UI:
|
||||
|
||||
- One canonical source of truth for shared chrome — compose it, never re-derive it per consumer.
|
||||
- Props-driven API over `className` overrides — reaching for `className` to change chrome is a smell; expose a prop instead.
|
||||
- Discriminated-union props for modes (e.g. `ChipDropdown multiple`) over near-duplicate components.
|
||||
- Delete legacy variants/components after migration — no parallel paths left behind.
|
||||
- Plain `cn()` for a single error/state toggle; CVA only for genuinely multiple variants.
|
||||
- Align consumers to the canonical defaults — normal weight, `--text-body` text, `--text-icon` icons.
|
||||
- Verify referenced CSS vars exist — an undefined var silently falls back to `currentColor` (black-bug).
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -370,7 +407,7 @@ Use Vitest. Test files: `feature.ts` → `feature.test.ts`. See `.cursor/rules/s
|
||||
|
||||
### Global Mocks (vitest.setup.ts)
|
||||
|
||||
`@sim/db`, `drizzle-orm`, `@sim/logger`, `@/blocks/registry`, `@trigger.dev/sdk`, and store mocks are provided globally. Do NOT re-mock them unless overriding behavior.
|
||||
`@sim/db`, `@sim/db/schema`, `drizzle-orm`, `@sim/logger`, `@sim/workflow-authz`, `@/blocks/registry`, `@/lib/auth`, `@/lib/auth/hybrid`, `@/lib/core/utils/request`, `@trigger.dev/sdk`, and store mocks are provided globally. Do NOT re-mock them unless overriding behavior. (The `vi.mock('@/lib/auth', ...)` in the example below is an override of the global mock so `getSession` can be controlled per-test.)
|
||||
|
||||
### Standard Test Pattern
|
||||
|
||||
@@ -420,126 +457,12 @@ Use `@sim/testing` mocks/factories over local test data.
|
||||
|
||||
## Adding Integrations
|
||||
|
||||
New integrations require: **Tools** → **Block** → **Icon** → (optional) **Trigger**
|
||||
New integrations are built in order: **Tools** → **Block** → **Icon** → (optional) **Trigger**. Always look up the service's API docs first.
|
||||
|
||||
Always look up the service's API docs first.
|
||||
Two hard rules that the skills assume:
|
||||
|
||||
### 1. Tools (`tools/{service}/`)
|
||||
- **Tool IDs are `snake_case`** (`service_action`) and must be registered in `tools/registry.ts`; blocks register in `blocks/registry.ts` (alphabetically).
|
||||
- **`tools.config.tool` runs during serialization (before variable resolution)** — never do `Number()` or other type coercions there, or dynamic references like `<Block.output>` are destroyed. Put all type coercions in `tools.config.params`, which runs during execution after variables resolve.
|
||||
|
||||
```
|
||||
tools/{service}/
|
||||
├── index.ts # Barrel export
|
||||
├── types.ts # Params/response types
|
||||
└── {action}.ts # Tool implementation
|
||||
```
|
||||
|
||||
**Tool structure:**
|
||||
|
||||
```typescript
|
||||
export const serviceTool: ToolConfig<Params, Response> = {
|
||||
id: 'service_action',
|
||||
name: 'Service Action',
|
||||
description: '...',
|
||||
version: '1.0.0',
|
||||
oauth: { required: true, provider: 'service' },
|
||||
params: { /* ... */ },
|
||||
request: { url: '/api/tools/service/action', method: 'POST', ... },
|
||||
transformResponse: async (response) => { /* ... */ },
|
||||
outputs: { /* ... */ },
|
||||
}
|
||||
```
|
||||
|
||||
Register in `tools/registry.ts`.
|
||||
|
||||
### 2. Block (`blocks/blocks/{service}.ts`)
|
||||
|
||||
```typescript
|
||||
export const ServiceBlock: BlockConfig = {
|
||||
type: 'service',
|
||||
name: 'Service',
|
||||
description: '...',
|
||||
category: 'tools',
|
||||
bgColor: '#hexcolor',
|
||||
icon: ServiceIcon,
|
||||
subBlocks: [ /* see SubBlock Properties */ ],
|
||||
tools: { access: ['service_action'], config: { tool: (p) => `service_${p.operation}`, params: (p) => ({ /* type coercions here */ }) } },
|
||||
inputs: { /* ... */ },
|
||||
outputs: { /* ... */ },
|
||||
}
|
||||
```
|
||||
|
||||
Register in `blocks/registry.ts` (alphabetically).
|
||||
|
||||
**Important:** `tools.config.tool` runs during serialization (before variable resolution). Never do `Number()` or other type coercions there — dynamic references like `<Block.output>` will be destroyed. Use `tools.config.params` for type coercions (it runs during execution, after variables are resolved).
|
||||
|
||||
**SubBlock Properties:**
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'field', title: 'Label', type: 'short-input', placeholder: '...',
|
||||
required: true, // or condition object
|
||||
condition: { field: 'op', value: 'send' }, // show/hide
|
||||
dependsOn: ['credential'], // clear when dep changes
|
||||
mode: 'basic', // 'basic' | 'advanced' | 'both' | 'trigger'
|
||||
}
|
||||
```
|
||||
|
||||
**condition examples:**
|
||||
|
||||
- `{ field: 'op', value: 'send' }` - show when op === 'send'
|
||||
- `{ field: 'op', value: ['a','b'] }` - show when op is 'a' OR 'b'
|
||||
- `{ field: 'op', value: 'x', not: true }` - show when op !== 'x'
|
||||
- `{ field: 'op', value: 'x', not: true, and: { field: 'type', value: 'dm', not: true } }` - complex
|
||||
|
||||
**dependsOn:** `['field']` or `{ all: ['a'], any: ['b', 'c'] }`
|
||||
|
||||
**File Input Pattern (basic/advanced mode):**
|
||||
|
||||
```typescript
|
||||
// Basic: file-upload UI
|
||||
{ id: 'uploadFile', type: 'file-upload', canonicalParamId: 'file', mode: 'basic' },
|
||||
// Advanced: reference from other blocks
|
||||
{ id: 'fileRef', type: 'short-input', canonicalParamId: 'file', mode: 'advanced' },
|
||||
```
|
||||
|
||||
In `tools.config.tool`, normalize with:
|
||||
|
||||
```typescript
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
const file = normalizeFileInput(params.uploadFile || params.fileRef, { single: true })
|
||||
if (file) params.file = file
|
||||
```
|
||||
|
||||
For file uploads, create an internal API route (`/api/tools/{service}/upload`) that uses `downloadFileFromStorage` to get file content from `UserFile` objects.
|
||||
|
||||
### 3. Icon (`components/icons.tsx`)
|
||||
|
||||
```typescript
|
||||
export function ServiceIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return <svg {...props}>/* SVG from brand assets */</svg>
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Trigger (`triggers/{service}/`) - Optional
|
||||
|
||||
```
|
||||
triggers/{service}/
|
||||
├── index.ts # Barrel export
|
||||
├── webhook.ts # Webhook handler
|
||||
└── {event}.ts # Event-specific handlers
|
||||
```
|
||||
|
||||
Register in `triggers/registry.ts`.
|
||||
|
||||
### Integration Checklist
|
||||
|
||||
- Look up API docs
|
||||
- Create `tools/{service}/` with types and tools
|
||||
- Register tools in `tools/registry.ts`
|
||||
- Add icon to `components/icons.tsx`
|
||||
- Create block in `blocks/blocks/{service}.ts`
|
||||
- Register block in `blocks/registry.ts`
|
||||
- (Optional) Create and register triggers
|
||||
- (If file uploads) Create internal API route with `downloadFileFromStorage`
|
||||
- (If file uploads) Use `normalizeFileInput` in block config
|
||||
For the full authoring instructions — SubBlock property tables, `condition`/`dependsOn`/`required`/`mode`/`canonicalParamId` syntax, required block metadata (`integrationType`, `tags`, `authMode`, `docsLink`, `{Service}BlockMeta`), file-input/`normalizeFileInput` patterns, and checklists — use the skills: `/add-integration` (end-to-end), `/add-tools`, `/add-block`, `/add-trigger`.
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ const OG_FOOTER_STYLE = {
|
||||
width: '100%',
|
||||
} satisfies CSSProperties
|
||||
const OG_DOMAIN_STYLE = {
|
||||
fontSize: 20,
|
||||
fontSize: 24,
|
||||
fontWeight: 400,
|
||||
color: '#71717a',
|
||||
} satisfies CSSProperties
|
||||
@@ -84,7 +84,7 @@ async function loadGoogleFont(font: string, weights: string, text: string): Prom
|
||||
*/
|
||||
function SimLogoFull() {
|
||||
return (
|
||||
<svg height='28' viewBox='720 440 1020 320' fill='none'>
|
||||
<svg height='34' viewBox='720 440 1020 320' fill='none'>
|
||||
{/* Green icon - top left shape with cutout */}
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 15 KiB |
@@ -45,6 +45,7 @@ export const metadata = {
|
||||
classification: 'Developer Documentation',
|
||||
manifest: '/favicon/site.webmanifest',
|
||||
icons: {
|
||||
icon: [{ url: '/icon.svg', type: 'image/svg+xml', sizes: 'any' }],
|
||||
apple: '/favicon/apple-touch-icon.png',
|
||||
},
|
||||
appleWebApp: {
|
||||
|
||||
@@ -1707,7 +1707,7 @@ export function AlgoliaIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 50 50'>
|
||||
<path
|
||||
fill='#FFFFFF'
|
||||
fill='currentColor'
|
||||
d='M25,0C11.3,0,0.2,11,0,24.6C-0.2,38.4,11,49.9,24.8,50c4.3,0,8.4-1,12-3c0.4-0.2,0.4-0.7,0.1-1l-2.3-2.1 c-0.5-0.4-1.2-0.5-1.7-0.3c-2.5,1.1-5.3,1.6-8.2,1.6c-11.2-0.1-20.2-9.4-20-20.6C4.9,13.6,13.9,4.7,25,4.7h20.3v36L33.7,30.5 c-0.4-0.3-0.9-0.3-1.2,0.1c-1.8,2.4-4.9,4-8.2,3.7c-4.6-0.3-8.4-4-8.7-8.7c-0.4-5.5,4-10.2,9.4-10.2c4.9,0,9,3.8,9.4,8.6 c0,0.4,0.2,0.8,0.6,1.1l3,2.7c0.3,0.3,0.9,0.1,1-0.3c0.2-1.2,0.3-2.4,0.2-3.6c-0.5-7-6.2-12.7-13.2-13.1c-8.1-0.5-14.8,5.8-15,13.7 c-0.2,7.7,6.1,14.4,13.8,14.5c3.2,0.1,6.2-0.9,8.6-2.7l15,13.3c0.6,0.6,1.7,0.1,1.7-0.7v-48C50,0.4,49.5,0,49,0L25,0 C25,0,25,0,25,0z'
|
||||
/>
|
||||
</svg>
|
||||
@@ -2729,15 +2729,15 @@ export function ClayIcon(props: SVGProps<SVGSVGElement>) {
|
||||
export function ClerkIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 128 128' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<circle cx='64' cy='64' r='20' fill='white' />
|
||||
<circle cx='64' cy='64' r='20' fill='currentColor' />
|
||||
<path
|
||||
d='M99.57 10.79C101.57 12.13 101.74 14.94 100.04 16.65L85.42 31.26C84.1 32.58 82.05 32.79 80.39 31.94C75.48 29.42 69.9 28 64 28C44.12 28 28 44.12 28 64C28 69.9 29.42 75.48 31.94 80.39C32.79 82.05 32.58 84.1 31.26 85.42L16.65 100.04C14.94 101.74 12.13 101.57 10.79 99.57C3.97 89.4 0 77.16 0 64C0 28.65 28.65 0 64 0C77.16 0 89.4 3.97 99.57 10.79Z'
|
||||
fill='white'
|
||||
fill='currentColor'
|
||||
fillOpacity='0.4'
|
||||
/>
|
||||
<path
|
||||
d='M100.04 111.35C101.74 113.06 101.57 115.87 99.57 117.21C89.4 124.03 77.16 128 64 128C50.84 128 38.6 124.03 28.43 117.21C26.43 115.87 26.26 113.06 27.96 111.35L42.58 96.74C43.9 95.42 45.95 95.21 47.61 96.06C52.52 98.58 58.1 100 64 100C69.9 100 75.48 98.58 80.39 96.06C82.05 95.21 84.1 95.42 85.42 96.74L100.04 111.35Z'
|
||||
fill='white'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
@@ -2967,7 +2967,7 @@ export function PagerDutyIcon(props: SVGProps<SVGSVGElement>) {
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64' fill='none'>
|
||||
<path
|
||||
d='M6.7 59.22H0v-33.65c0-3.45 1.42-5.54 2.6-6.7 2.63-2.58 6.2-2.66 6.78-2.66h10.55c3.77 0 5.93 1.52 7.12 2.8 2.35 2.55 2.37 5.85 2.32 6.73v12.69c0 3.66-1.5 5.83-2.73 6.99-2.55 2.4-5.93 2.45-6.73 2.42H6.7zm13.46-18.1c.36 0 1.37-.103 1.91-.62.41-.387.62-1.08.62-2.1v-13.02c0-.36-.077-1.31-.593-1.86-.5-.516-1.44-.62-2.17-.62h-10.6c-2.63 0-2.63 1.99-2.63 2.66v15.55zM57.3 4.78H64V38.46c0 3.46-1.42 5.54-2.6 6.7-2.63 2.58-6.2 2.66-6.78 2.66H44.07c-3.76 0-5.93-1.52-7.12-2.8-2.35-2.55-2.37-5.85-2.32-6.73V25.62c0-3.66 1.5-5.83 2.73-6.99 2.55-2.4 5.93-2.45 6.73-2.42h13.2zM43.84 22.9c-.36 0-1.37.103-1.91.62-.413.39-.62 1.08-.62 2.1v13.02c0 .36.08 1.32.593 1.86.5.52 1.44.62 2.17.62h10.6c2.66-.026 2.66-2 2.66-2.68V22.9z'
|
||||
fill='#FFFFFF'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
@@ -4502,7 +4502,7 @@ export function PipedriveIcon(props: SVGProps<SVGSVGElement>) {
|
||||
fillRule='evenodd'
|
||||
>
|
||||
<g transform='translate(67, 44)'>
|
||||
<use fill='#FFFFFF' xlinkHref={`#${pathId}`} />
|
||||
<use fill='currentColor' xlinkHref={`#${pathId}`} />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
@@ -6373,7 +6373,7 @@ export function ReductoIcon(props: SVGProps<SVGSVGElement>) {
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M85.34 70.78H314.66V240.31L226.44 329.22H85.34V70.78ZM107.8 93.23H292.21V204.49H206.49V306.77H107.8L107.8 93.23Z'
|
||||
fill='#FFFFFF'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
@@ -7247,11 +7247,11 @@ export function RevenueCatIcon(props: SVGProps<SVGSVGElement>) {
|
||||
>
|
||||
<path
|
||||
d='M95 109.77C110.15 106.11 133.61 104 154.79 104C212.05 104 246.32 123.93 246.32 174.65C246.32 205.75 233.74 226.26 214.01 237.44L261.77 318.95C258.05 321.63 250.04 323.18 238.86 323.18C226.28 323.18 217.99 321.67 211.98 318.95L172.23 248.3H167.65C157.79 248.31 147.95 247.6 138.18 246.19V319.26C134.17 321.67 127.02 323.18 116.73 323.18C106.73 323.18 99.29 321.66 95 319.26V109.77ZM137.64 207.85C145.77 209.26 154 209.97 162.24 209.96C187.12 209.96 202.29 200.56 202.29 177.06C202.29 152.89 186.27 142.95 157.67 142.95C150.96 142.92 144.26 143.51 137.64 144.74V207.85Z'
|
||||
fill='#FFFFFF'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
d='M428.53 329.24C428.53 365.53 410.15 375.49 396.31 382.2C360.97 399.32 304.37 379.4 244.21 373.34C189.73 366.21 135.71 361.52 127.31 373.74C124.15 376.83 123.48 386.8 127.31 390.86C138.6 402.85 168.06 394.49 188.92 390.71C195.39 389.69 201.93 392.1 206.08 397.02C210.23 401.94 211.35 408.64 209.02 414.58C206.7 420.52 201.28 424.81 194.81 425.83C185.38 427.26 175.85 427.99 166.31 428C145.99 428 120.44 424.5 105.94 409.07C98.72 401.4 91.33 387.78 97.03 366.47C107.88 326.07 172.81 336.05 248.03 343.63C300.41 348.91 357.23 366.47 379.93 350.34C385.72 346.23 396.52 337.02 390.7 329.24C384.88 321.47 375.35 325.68 362.84 325.68C300.15 325.68 263.24 285.3 263.24 217.92C263.25 167.29 284.18 131.89 318.29 115.09C333.11 107.79 350.42 104 369.59 104C386.29 104 403.27 106.93 414.11 113.37C420.85 123.03 423.78 140.31 422.31 153.2C408.25 146.47 395.36 142.95 378.67 142.95C337.37 142.95 308.95 164.04 308.95 214.99C308.95 265.93 337.07 286.15 376.61 286.15C387.87 286.04 403.1 284.67 422.31 282.05C426.46 297.5 428.53 313.23 428.53 329.24Z'
|
||||
fill='#FFFFFF'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
@@ -7270,7 +7270,7 @@ export function RedisIcon(props: SVGProps<SVGSVGElement>) {
|
||||
>
|
||||
<path
|
||||
d='M479.14 279.86c-34.58 43.58-71.94 93.39-146.65 93.39-66.73 0-91.59-58.86-93.34-106.67 14.62 30.92 43.2 55.95 87.8 54.79C412.74 318.6 471.53 241.13 471.53 170.57c0-84.39-62.95-145.26-172.24-145.26-78.17 0-175 29.74-238.65 76.78-.689 48.42 26.29 111.37 35.97 104.45 55.17-39.67 98.92-65.2 141.35-78.01C175.15 198.58 24.45 361.22 6 389.85c2.08 26.29 34.59 96.84 50.5 96.84 4.84 0 8.99-2.77 13.84-7.61 45.43-51.05 82.47-96.82 115.41-140.93 4.63 64.66 36.42 143.7 125.31 143.7 79.55 0 158.41-57.41 194.38-186.77 4.15-15.91-15.22-28.36-26.29-15.22zm-90.62-104.45c0 40.81-40.12 60.87-76.78 60.87-19.6 0-34.65-5.14-46.55-11.83 21.91-33.17 43.59-67.18 66.89-103.59 41.08 6.95 56.45 29.79 56.45 54.56z'
|
||||
fill='#FFFFFF'
|
||||
fill='currentColor'
|
||||
fillRule='nonzero'
|
||||
/>
|
||||
</svg>
|
||||
@@ -7385,7 +7385,7 @@ export function WizaIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 51 49' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
fill='#FFFFFF'
|
||||
fill='currentColor'
|
||||
d='m50.3 33.6-9.3-3.5c-2.9-10.4-9.6-20.8-17.1-29.4-1.4-1.5-4.4-0.7-4.2 1.9 0.4 6-0.4 13.3-4.4 20.5-0.9 1.6 1.4 3.3 2.9 1.6 3.2-3.3 5.3-9.1 6.3-15.2 3.9 5.4 9.2 13.8 11.2 22.9 0.2 0.9 0.8 1.6 1.6 1.9l6.2 2.3c-4.1 3.3-9.6 6-17.7 6-7.1 0-13.2-2.3-18.5-6l3.6-2.5c3.5 2 7.5 3.5 12.5 3.6 2.5 0 5-0.2 7.3-0.9 2-0.5 1.6-3.2-0.3-3-4 0.5-10.3 0.3-17.1-4.3-0.8-0.5-1.9-0.6-2.7-0.2l-9.2 4.3c-1.4 0.6-2.1 3.1-0.5 4.3 4.7 3.8 12.4 10.1 24.2 10.1 11.9 0 20.4-4.3 25.8-10.1 1.2-1.2 1.2-3.5-0.6-4.3z'
|
||||
/>
|
||||
</svg>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
import type { ComponentType, SVGProps } from 'react'
|
||||
import {
|
||||
A2AIcon,
|
||||
AgentMailIcon,
|
||||
AgentPhoneIcon,
|
||||
AgiloftIcon,
|
||||
@@ -97,7 +96,6 @@ import {
|
||||
HunterIOIcon,
|
||||
IAMIcon,
|
||||
IdentityCenterIcon,
|
||||
ImageIcon,
|
||||
IncidentioIcon,
|
||||
InfisicalIcon,
|
||||
InstantlyIcon,
|
||||
@@ -129,7 +127,6 @@ import {
|
||||
MistralIcon,
|
||||
MondayIcon,
|
||||
MongoDBIcon,
|
||||
MySQLIcon,
|
||||
Neo4jIcon,
|
||||
NewRelicIcon,
|
||||
NotionIcon,
|
||||
@@ -146,7 +143,6 @@ import {
|
||||
PineconeIcon,
|
||||
PipedriveIcon,
|
||||
PolymarketIcon,
|
||||
PostgresIcon,
|
||||
PosthogIcon,
|
||||
ProfoundIcon,
|
||||
ProspeoIcon,
|
||||
@@ -168,20 +164,16 @@ import {
|
||||
SapConcurIcon,
|
||||
SapS4HanaIcon,
|
||||
SESIcon,
|
||||
SearchIcon,
|
||||
SecretsManagerIcon,
|
||||
SendgridIcon,
|
||||
SentryIcon,
|
||||
SerperIcon,
|
||||
ServiceNowIcon,
|
||||
SftpIcon,
|
||||
ShopifyIcon,
|
||||
SimilarwebIcon,
|
||||
SixtyfourIcon,
|
||||
SlackIcon,
|
||||
SmtpIcon,
|
||||
SQSIcon,
|
||||
SshIcon,
|
||||
STSIcon,
|
||||
STTIcon,
|
||||
StagehandIcon,
|
||||
@@ -192,9 +184,7 @@ import {
|
||||
TelegramIcon,
|
||||
TextractIcon,
|
||||
TinybirdIcon,
|
||||
TranslateIcon,
|
||||
TrelloIcon,
|
||||
TTSIcon,
|
||||
TwilioIcon,
|
||||
TypeformIcon,
|
||||
UpstashIcon,
|
||||
@@ -218,7 +208,6 @@ import {
|
||||
type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
|
||||
|
||||
export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
a2a: A2AIcon,
|
||||
agentmail: AgentMailIcon,
|
||||
agentphone: AgentPhoneIcon,
|
||||
agiloft: AgiloftIcon,
|
||||
@@ -276,7 +265,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
fathom: FathomIcon,
|
||||
file: DocumentIcon,
|
||||
file_v2: DocumentIcon,
|
||||
file_v3: DocumentIcon,
|
||||
file_v4: DocumentIcon,
|
||||
findymail: FindymailIcon,
|
||||
firecrawl: FirecrawlIcon,
|
||||
@@ -321,8 +309,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
hunter: HunterIOIcon,
|
||||
iam: IAMIcon,
|
||||
identity_center: IdentityCenterIcon,
|
||||
image_generator: ImageIcon,
|
||||
image_generator_v2: ImageIcon,
|
||||
imap: MailServerIcon,
|
||||
incidentio: IncidentioIcon,
|
||||
infisical: InfisicalIcon,
|
||||
@@ -361,7 +347,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
mistral_parse_v3: MistralIcon,
|
||||
monday: MondayIcon,
|
||||
mongodb: MongoDBIcon,
|
||||
mysql: MySQLIcon,
|
||||
neo4j: Neo4jIcon,
|
||||
new_relic: NewRelicIcon,
|
||||
notion: NotionIcon,
|
||||
@@ -379,7 +364,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
pinecone: PineconeIcon,
|
||||
pipedrive: PipedriveIcon,
|
||||
polymarket: PolymarketIcon,
|
||||
postgresql: PostgresIcon,
|
||||
posthog: PosthogIcon,
|
||||
profound: ProfoundIcon,
|
||||
prospeo: ProspeoIcon,
|
||||
@@ -402,23 +386,19 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
salesforce: SalesforceIcon,
|
||||
sap_concur: SapConcurIcon,
|
||||
sap_s4hana: SapS4HanaIcon,
|
||||
search: SearchIcon,
|
||||
secrets_manager: SecretsManagerIcon,
|
||||
sendgrid: SendgridIcon,
|
||||
sentry: SentryIcon,
|
||||
serper: SerperIcon,
|
||||
servicenow: ServiceNowIcon,
|
||||
ses: SESIcon,
|
||||
sftp: SftpIcon,
|
||||
sharepoint: MicrosoftSharepointIcon,
|
||||
sharepoint_v2: MicrosoftSharepointIcon,
|
||||
shopify: ShopifyIcon,
|
||||
similarweb: SimilarwebIcon,
|
||||
sixtyfour: SixtyfourIcon,
|
||||
slack: SlackIcon,
|
||||
smtp: SmtpIcon,
|
||||
sqs: SQSIcon,
|
||||
ssh: SshIcon,
|
||||
stagehand: StagehandIcon,
|
||||
stripe: StripeIcon,
|
||||
sts: STSIcon,
|
||||
@@ -431,9 +411,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
textract: TextractIcon,
|
||||
textract_v2: TextractIcon,
|
||||
tinybird: TinybirdIcon,
|
||||
translate: TranslateIcon,
|
||||
trello: TrelloIcon,
|
||||
tts: TTSIcon,
|
||||
twilio_sms: TwilioIcon,
|
||||
twilio_voice: TwilioIcon,
|
||||
typeform: TypeformIcon,
|
||||
@@ -441,7 +419,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
vercel: VercelIcon,
|
||||
video_generator: VideoIcon,
|
||||
video_generator_v2: VideoIcon,
|
||||
video_generator_v3: VideoIcon,
|
||||
vision: EyeIcon,
|
||||
vision_v2: EyeIcon,
|
||||
wealthbox: WealthboxIcon,
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
---
|
||||
title: Formular-Bereitstellung
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
|
||||
Stellen Sie Ihren Workflow als einbettbares Formular bereit, das Benutzer auf Ihrer Website ausfüllen oder per Link teilen können. Formularübermittlungen lösen Ihren Workflow mit dem `form` Trigger-Typ aus.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Die Formular-Bereitstellung verwandelt das Eingabeformat Ihres Workflows in ein responsives Formular, das:
|
||||
- Per Direktlink geteilt werden kann (z. B. `https://sim.ai/form/my-survey`)
|
||||
- Mit einem iframe in jede Website eingebettet werden kann
|
||||
|
||||
Wenn ein Benutzer das Formular absendet, wird Ihr Workflow mit den Formulardaten ausgelöst.
|
||||
|
||||
<Callout type="info">
|
||||
Formulare leiten ihre Felder vom Eingabeformat des Start-Blocks Ihres Workflows ab. Jedes Feld wird zu einer Formulareingabe mit dem entsprechenden Typ.
|
||||
</Callout>
|
||||
|
||||
## Erstellen eines Formulars
|
||||
|
||||
1. Öffnen Sie Ihren Workflow und klicken Sie auf **Bereitstellen**
|
||||
2. Wählen Sie den Tab **Formular**
|
||||
3. Konfigurieren Sie:
|
||||
- **URL**: Eindeutige Kennung (z. B. `contact-form` → `sim.ai/form/contact-form`)
|
||||
- **Titel**: Formularüberschrift
|
||||
- **Beschreibung**: Optionaler Untertitel
|
||||
- **Formularfelder**: Passen Sie Beschriftungen und Beschreibungen für jedes Feld an
|
||||
- **Authentifizierung**: Öffentlich, passwortgeschützt oder E-Mail-Whitelist
|
||||
- **Dankesnachricht**: Wird nach der Übermittlung angezeigt
|
||||
4. Klicken Sie auf **Starten**
|
||||
|
||||
## Feldzuordnung
|
||||
|
||||
| Eingabeformat-Typ | Formularfeld |
|
||||
|------------------|------------|
|
||||
| `string` | Texteingabe |
|
||||
| `number` | Zahleneingabe |
|
||||
| `boolean` | Umschalter |
|
||||
| `object` | JSON-Editor |
|
||||
| `array` | JSON-Array-Editor |
|
||||
| `files` | Datei-Upload |
|
||||
|
||||
## Zugriffskontrolle
|
||||
|
||||
| Modus | Beschreibung |
|
||||
|------|-------------|
|
||||
| **Öffentlich** | Jeder mit dem Link kann absenden |
|
||||
| **Passwort** | Benutzer müssen ein Passwort eingeben |
|
||||
| **E-Mail-Whitelist** | Nur angegebene E-Mails/Domains können absenden |
|
||||
|
||||
Für E-Mail-Whitelist:
|
||||
- Exakt: `user@example.com`
|
||||
- Domain: `@example.com` (alle E-Mails von der Domain)
|
||||
|
||||
## Einbettung
|
||||
|
||||
### Direkter Link
|
||||
|
||||
```
|
||||
https://sim.ai/form/your-identifier
|
||||
```
|
||||
|
||||
### Iframe
|
||||
|
||||
```html
|
||||
<iframe
|
||||
src="https://sim.ai/form/your-identifier"
|
||||
width="100%"
|
||||
height="600"
|
||||
frameborder="0"
|
||||
title="Form"
|
||||
></iframe>
|
||||
```
|
||||
|
||||
## API-Übermittlung
|
||||
|
||||
Formulare programmatisch übermitteln:
|
||||
|
||||
<Tabs items={['cURL', 'TypeScript']}>
|
||||
<Tab value="cURL">
|
||||
|
||||
```bash
|
||||
curl -X POST https://sim.ai/api/form/your-identifier \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"formData": {
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
|
||||
```typescript
|
||||
const response = await fetch('https://sim.ai/api/form/your-identifier', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
formData: {
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com'
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
// { success: true, data: { executionId: '...' } }
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Geschützte Formulare
|
||||
|
||||
Für passwortgeschützte Formulare:
|
||||
|
||||
```bash
|
||||
curl -X POST https://sim.ai/api/form/your-identifier \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{ "password": "secret", "formData": { "name": "John" } }'
|
||||
```
|
||||
|
||||
Für E-Mail-geschützte Formulare:
|
||||
|
||||
```bash
|
||||
curl -X POST https://sim.ai/api/form/your-identifier \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{ "email": "allowed@example.com", "formData": { "name": "John" } }'
|
||||
```
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
**"Keine Eingabefelder konfiguriert"** - Fügen Sie Eingabeformat-Felder zu Ihrem Start-Block hinzu.
|
||||
|
||||
**Formular lädt nicht im Iframe** - Überprüfen Sie, ob die CSP Ihrer Website Iframes von `sim.ai` erlaubt.
|
||||
|
||||
**Übermittlungen schlagen fehl** - Überprüfen Sie, ob die Kennung korrekt ist und erforderliche Felder ausgefüllt sind.
|
||||
@@ -9,10 +9,12 @@
|
||||
"function",
|
||||
"guardrails",
|
||||
"human-in-the-loop",
|
||||
"knowledge",
|
||||
"loop",
|
||||
"parallel",
|
||||
"response",
|
||||
"router",
|
||||
"table",
|
||||
"variables",
|
||||
"wait",
|
||||
"webhook",
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
---
|
||||
title: Form Deployment
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
|
||||
Deploy your workflow as an embeddable form that users can fill out on your website or share via link. Form submissions trigger your workflow with the `form` trigger type.
|
||||
|
||||
## Overview
|
||||
|
||||
Form deployment turns your workflow's Input Format into a responsive form that can be:
|
||||
- Shared via a direct link (e.g., `https://sim.ai/form/my-survey`)
|
||||
- Embedded in any website using an iframe
|
||||
|
||||
When a user submits the form, it triggers your workflow with the form data.
|
||||
|
||||
<Callout type="info">
|
||||
Forms derive their fields from your workflow's Start block Input Format. Each field becomes a form input with the appropriate type.
|
||||
</Callout>
|
||||
|
||||
## Creating a Form
|
||||
|
||||
1. Open your workflow and click **Deploy**
|
||||
2. Select the **Form** tab
|
||||
3. Configure:
|
||||
- **URL**: Unique identifier (e.g., `contact-form` → `sim.ai/form/contact-form`)
|
||||
- **Title**: Form heading
|
||||
- **Description**: Optional subtitle
|
||||
- **Form Fields**: Customize labels and descriptions for each field
|
||||
- **Authentication**: Public, password-protected, or email whitelist
|
||||
- **Thank You Message**: Shown after submission
|
||||
4. Click **Launch**
|
||||
|
||||
## Field Type Mapping
|
||||
|
||||
| Input Format Type | Form Field |
|
||||
|------------------|------------|
|
||||
| `string` | Text input |
|
||||
| `number` | Number input |
|
||||
| `boolean` | Toggle switch |
|
||||
| `object` | JSON editor |
|
||||
| `array` | JSON array editor |
|
||||
| `files` | File upload |
|
||||
|
||||
## Access Control
|
||||
|
||||
| Mode | Description |
|
||||
|------|-------------|
|
||||
| **Public** | Anyone with the link can submit |
|
||||
| **Password** | Users must enter a password |
|
||||
| **Email Whitelist** | Only specified emails/domains can submit |
|
||||
|
||||
For email whitelist:
|
||||
- Exact: `user@example.com`
|
||||
- Domain: `@example.com` (all emails from domain)
|
||||
|
||||
## Embedding
|
||||
|
||||
### Direct Link
|
||||
|
||||
```
|
||||
https://sim.ai/form/your-identifier
|
||||
```
|
||||
|
||||
### Iframe
|
||||
|
||||
```html
|
||||
<iframe
|
||||
src="https://sim.ai/form/your-identifier"
|
||||
width="100%"
|
||||
height="600"
|
||||
frameborder="0"
|
||||
title="Form"
|
||||
></iframe>
|
||||
```
|
||||
|
||||
## API Submission
|
||||
|
||||
Submit forms programmatically:
|
||||
|
||||
<Tabs items={['cURL', 'TypeScript']}>
|
||||
<Tab value="cURL">
|
||||
```bash
|
||||
curl -X POST https://sim.ai/api/form/your-identifier \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"formData": {
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com"
|
||||
}
|
||||
}'
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const response = await fetch('https://sim.ai/api/form/your-identifier', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
formData: {
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com'
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
// { success: true, data: { executionId: '...' } }
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Protected Forms
|
||||
|
||||
For password-protected forms:
|
||||
```bash
|
||||
curl -X POST https://sim.ai/api/form/your-identifier \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{ "password": "secret", "formData": { "name": "John" } }'
|
||||
```
|
||||
|
||||
For email-protected forms:
|
||||
```bash
|
||||
curl -X POST https://sim.ai/api/form/your-identifier \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{ "email": "allowed@example.com", "formData": { "name": "John" } }'
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"No input fields configured"** - Add Input Format fields to your Start block.
|
||||
|
||||
**Form not loading in iframe** - Check your site's CSP allows iframes from `sim.ai`.
|
||||
|
||||
**Submissions failing** - Verify the identifier is correct and required fields are filled.
|
||||
@@ -51,10 +51,10 @@ Export a full table or a filtered subset as a CSV. The file is saved to your wor
|
||||
|
||||
## Using Tables in Workflows
|
||||
|
||||
Tables created in Mothership are immediately available in workflows via the [Table tool](/tools/table). Reference a table by name — no additional configuration needed.
|
||||
Tables created in Mothership are immediately available in workflows via the [Table block](/blocks/table). Reference a table by name — no additional configuration needed.
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "Can Mothership join or combine data from multiple tables?", answer: "Yes. Describe the relationship and what you want — Mothership will query both tables and combine the results." },
|
||||
{ question: "Is there a row limit?", answer: "There is no hard row limit set by Mothership. Performance for very large tables may vary." },
|
||||
{ question: "Can I use a table created in Mothership as a workflow data source?", answer: "Yes. All workspace tables are accessible to the Table tool in any workflow." },
|
||||
{ question: "Can I use a table created in Mothership as a workflow data source?", answer: "Yes. All workspace tables are accessible to the Table block in any workflow." },
|
||||
]} />
|
||||
|
||||
@@ -90,10 +90,6 @@ A quick lookup for everyday actions in the Sim workflow editor. For keyboard sho
|
||||
<td>Rename a workflow</td>
|
||||
<td>Right-click workflow → **Rename**</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Assign workflow color</td>
|
||||
<td>Right-click workflow → **Change Color**</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Duplicate a workflow</td>
|
||||
<td>Right-click workflow → **Duplicate**</td>
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
---
|
||||
title: A2A
|
||||
description: Interact with external A2A-compatible agents
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="a2a"
|
||||
color="#4151B5"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
The A2A (Agent-to-Agent) protocol enables Sim to interact with external AI agents and systems that implement A2A-compatible APIs. With A2A, you can connect Sim’s automations and workflows to remote agents—such as LLM-powered bots, microservices, and other AI-based tools—using a standardized messaging format.
|
||||
|
||||
Using the A2A tools in Sim, you can:
|
||||
|
||||
- **Send Messages to External Agents**: Communicate directly with remote agents, providing prompts, commands, or data.
|
||||
- **Receive and Stream Responses**: Get structured responses, artifacts, or real-time updates from the agent as the task progresses.
|
||||
- **Continue Conversations or Tasks**: Carry on multi-turn conversations or workflows by referencing task and context IDs.
|
||||
- **Integrate Third-Party AI and Automation**: Leverage external A2A-compatible services as part of your Sim workflows.
|
||||
|
||||
These features allow you to build advanced workflows that combine Sim’s native capabilities with the intelligence and automation of external AIs or custom agents. To use A2A integrations, you’ll need the external agent’s endpoint URL and, if required, an API key or credentials.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Use the A2A (Agent-to-Agent) protocol to interact with external AI agents.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `a2a_send_message`
|
||||
|
||||
Send a message to an external A2A-compatible agent.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `agentUrl` | string | Yes | The A2A agent endpoint URL |
|
||||
| `message` | string | Yes | Message to send to the agent |
|
||||
| `taskId` | string | No | Task ID for continuing an existing task |
|
||||
| `contextId` | string | No | Context ID for conversation continuity |
|
||||
| `data` | string | No | Structured data to include with the message \(JSON string\) |
|
||||
| `files` | array | No | Files to include with the message |
|
||||
| `apiKey` | string | No | API key for authentication |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Text response content from the agent |
|
||||
| `taskId` | string | Unique task identifier |
|
||||
| `contextId` | string | Groups related tasks/messages |
|
||||
| `state` | string | Current lifecycle state \(working, completed, failed, canceled, rejected, input_required, auth_required\) |
|
||||
| `artifacts` | array | Task output artifacts |
|
||||
| `history` | array | Conversation history \(Message array\) |
|
||||
|
||||
### `a2a_get_task`
|
||||
|
||||
Query the status of an existing A2A task.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `agentUrl` | string | Yes | The A2A agent endpoint URL |
|
||||
| `taskId` | string | Yes | Task ID to query |
|
||||
| `apiKey` | string | No | API key for authentication |
|
||||
| `historyLength` | number | No | Number of history messages to include |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `taskId` | string | Unique task identifier |
|
||||
| `contextId` | string | Groups related tasks/messages |
|
||||
| `state` | string | Current lifecycle state \(working, completed, failed, canceled, rejected, input_required, auth_required\) |
|
||||
| `artifacts` | array | Task output artifacts |
|
||||
| `history` | array | Conversation history \(Message array\) |
|
||||
|
||||
### `a2a_cancel_task`
|
||||
|
||||
Cancel a running A2A task.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `agentUrl` | string | Yes | The A2A agent endpoint URL |
|
||||
| `taskId` | string | Yes | Task ID to cancel |
|
||||
| `apiKey` | string | No | API key for authentication |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `cancelled` | boolean | Whether cancellation was successful |
|
||||
| `state` | string | Current lifecycle state \(working, completed, failed, canceled, rejected, input_required, auth_required\) |
|
||||
|
||||
### `a2a_get_agent_card`
|
||||
|
||||
Fetch the Agent Card (discovery document) for an A2A agent.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `agentUrl` | string | Yes | The A2A agent endpoint URL |
|
||||
| `apiKey` | string | No | API key for authentication \(if required\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `name` | string | Agent display name |
|
||||
| `description` | string | Agent purpose/capabilities |
|
||||
| `url` | string | Service endpoint URL |
|
||||
| `provider` | object | Creator organization details |
|
||||
| `capabilities` | object | Feature support matrix |
|
||||
| `skills` | array | Available operations |
|
||||
| `version` | string | A2A protocol version supported by the agent |
|
||||
| `defaultInputModes` | array | Default input content types accepted by the agent |
|
||||
| `defaultOutputModes` | array | Default output content types produced by the agent |
|
||||
|
||||
### `a2a_resubscribe`
|
||||
|
||||
Reconnect to an ongoing A2A task stream after connection interruption.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `agentUrl` | string | Yes | The A2A agent endpoint URL |
|
||||
| `taskId` | string | Yes | Task ID to resubscribe to |
|
||||
| `apiKey` | string | No | API key for authentication |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `taskId` | string | Unique task identifier |
|
||||
| `contextId` | string | Groups related tasks/messages |
|
||||
| `state` | string | Current lifecycle state \(working, completed, failed, canceled, rejected, input_required, auth_required\) |
|
||||
| `isRunning` | boolean | Whether the task is still running |
|
||||
| `artifacts` | array | Task output artifacts |
|
||||
| `history` | array | Conversation history \(Message array\) |
|
||||
|
||||
### `a2a_set_push_notification`
|
||||
|
||||
Configure a webhook to receive task update notifications.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `agentUrl` | string | Yes | The A2A agent endpoint URL |
|
||||
| `taskId` | string | Yes | Task ID to configure notifications for |
|
||||
| `webhookUrl` | string | Yes | HTTPS webhook URL to receive notifications |
|
||||
| `token` | string | No | Token for webhook validation |
|
||||
| `apiKey` | string | No | API key for authentication |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `url` | string | HTTPS webhook URL for notifications |
|
||||
| `token` | string | Authentication token for webhook validation |
|
||||
| `success` | boolean | Whether the operation was successful |
|
||||
|
||||
### `a2a_get_push_notification`
|
||||
|
||||
Get the push notification webhook configuration for a task.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `agentUrl` | string | Yes | The A2A agent endpoint URL |
|
||||
| `taskId` | string | Yes | Task ID to get notification config for |
|
||||
| `apiKey` | string | No | API key for authentication |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `token` | string | Authentication token for webhook validation |
|
||||
| `exists` | boolean | Whether the resource exists |
|
||||
|
||||
### `a2a_delete_push_notification`
|
||||
|
||||
Delete the push notification webhook configuration for a task.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `agentUrl` | string | Yes | The A2A agent endpoint URL |
|
||||
| `taskId` | string | Yes | Task ID to delete notification config for |
|
||||
| `pushNotificationConfigId` | string | No | Push notification configuration ID to delete \(optional - server can derive from taskId\) |
|
||||
| `apiKey` | string | No | API key for authentication |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the operation was successful |
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="agentphone"
|
||||
color="#000000"
|
||||
color="linear-gradient(135deg, #1a1a1a 0%, #0a2a14 100%)"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="ahrefs"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="airtable"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="apify"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="arxiv"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="asana"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="brightdata"
|
||||
color="#FFFFFF"
|
||||
color="#0F4C81"
|
||||
/>
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
title: Circleback
|
||||
description: AI-powered meeting notes and action items
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="circleback"
|
||||
color="linear-gradient(180deg, #E0F7FA 0%, #FFFFFF 100%)"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Circleback](https://circleback.ai/) is an AI-powered platform that automates meeting notes, action items, transcripts, and recordings for your team. When a meeting is completed, Circleback processes the conversation and provides detailed notes and action items, along with a transcript and a recording (when available). This helps teams efficiently capture insights, distribute action items, and ensure nothing is missed—all seamlessly integrated into your workflows.
|
||||
|
||||
With the Sim Circleback integration, you can:
|
||||
|
||||
- **Receive detailed meeting notes and action items**: Automatically collect well-formatted meeting summaries and track actionable tasks discussed during your calls.
|
||||
- **Access complete meeting recordings and transcripts**: Get the full conversation and the associated recording, making it easy to review key moments or share with colleagues.
|
||||
- **Capture attendee information and meeting context**: Attendee lists, meeting metadata, and tags help keep your data organized and actionable.
|
||||
- **Deliver insights directly into your workflows**: Trigger automations or send Circleback data to other systems the moment a meeting is done, using Sim’s powerful webhook triggers.
|
||||
|
||||
**How it works in Sim:**
|
||||
Circleback uses webhook triggers: whenever a meeting is processed, data is pushed automatically to your agent or automation. You can build further automations based on:
|
||||
|
||||
- Meeting completed (all processed data available)
|
||||
- New notes (notes ready even before full meeting is processed)
|
||||
- Raw webhook integration for advanced use cases
|
||||
|
||||
**The following information is available in the Circleback meeting webhook payload:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|----------------|---------|----------------------------------------------------|
|
||||
| `id` | number | Circleback meeting ID |
|
||||
| `name` | string | Meeting title |
|
||||
| `url` | string | Virtual meeting URL (Zoom, Meet, Teams, etc.) |
|
||||
| `createdAt` | string | Meeting creation timestamp |
|
||||
| `duration` | number | Duration in seconds |
|
||||
| `recordingUrl` | string | Recording URL (valid 24 hours) |
|
||||
| `tags` | json | Array of tags |
|
||||
| `icalUid` | string | Calendar event ID |
|
||||
| `attendees` | json | Array of attendee objects |
|
||||
| `notes` | string | Meeting notes in Markdown |
|
||||
| `actionItems` | json | Array of action items |
|
||||
| `transcript` | json | Array of transcript segments |
|
||||
| `insights` | json | User-created insights |
|
||||
| `meeting` | json | Full meeting payload |
|
||||
|
||||
Whether you want to distribute instant summaries, log action items, or build custom workflows triggered by new meeting data, Circleback and Sim make it seamless to handle everything related to your meetings—automatically.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Receive meeting notes, action items, transcripts, and recordings when meetings are processed. Circleback uses webhooks to push data to your workflows.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="clay"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="confluence_v2"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
---
|
||||
title: Custom Tools
|
||||
description: Create your own tools with JavaScript code and use them in Agent blocks
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
Custom tools let you write your own JavaScript functions and make them available as callable tools in Agent blocks. This is useful when you need functionality that isn't covered by Sim's built-in integrations — for example, calling an internal API, performing a custom calculation, or transforming data in a specific way.
|
||||
|
||||
## How Custom Tools Work
|
||||
|
||||
A custom tool has two parts:
|
||||
|
||||
1. **Schema** — A JSON definition describing the tool's name, description, and parameters (using the OpenAI function-calling format). This tells the AI agent what the tool does and what inputs it expects.
|
||||
2. **Code** — A JavaScript function body that runs when the agent calls the tool. Parameters defined in the schema are available as variables in your code.
|
||||
|
||||
When an Agent block has access to a custom tool, the AI model decides when to call it based on the schema description and the conversation context — just like built-in tools.
|
||||
|
||||
## Creating a Custom Tool
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
|
||||
### Open Custom Tools settings
|
||||
|
||||
Navigate to **Settings → Custom Tools** in your workspace and click **Add**.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Define the schema
|
||||
|
||||
In the **Schema** tab, define your tool using JSON in the OpenAI function-calling format:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "Get the current weather for a city",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": {
|
||||
"type": "string",
|
||||
"description": "The city name"
|
||||
},
|
||||
"units": {
|
||||
"type": "string",
|
||||
"enum": ["celsius", "fahrenheit"],
|
||||
"description": "Temperature units"
|
||||
}
|
||||
},
|
||||
"required": ["city"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
You can use the AI wand button to generate a schema from a natural language description of what the tool should do.
|
||||
</Callout>
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Write the code
|
||||
|
||||
Switch to the **Code** tab and write the JavaScript function body. Parameters from your schema are available directly as variables:
|
||||
|
||||
```javascript
|
||||
const response = await fetch(
|
||||
`https://api.openweathermap.org/data/2.5/weather?q=${city}&units=${units === 'celsius' ? 'metric' : 'imperial'}&appid={{OPENWEATHER_API_KEY}}`
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
temperature: data.main.temp,
|
||||
description: data.weather[0].description,
|
||||
humidity: data.main.humidity
|
||||
};
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
You can also use the AI wand to generate code from a description. Environment variables are referenced with `{{KEY}}` syntax.
|
||||
</Callout>
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Save
|
||||
|
||||
Click **Save** to create the tool. It's now available to use in any Agent block across your workspace.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Using Custom Tools in Workflows
|
||||
|
||||
Once created, custom tools appear alongside built-in tools when configuring an Agent block:
|
||||
|
||||
1. Open an Agent block
|
||||
2. Click **Add Tools**
|
||||
3. Find your custom tool in the tool list
|
||||
4. The agent will call the tool when it determines it's relevant to the task
|
||||
|
||||
## Code Environment
|
||||
|
||||
### Available Features
|
||||
|
||||
- **Async/await** — Your code runs in an async context, so you can use `await` directly
|
||||
- **fetch()** — Make HTTP requests to external APIs
|
||||
- **Node.js built-ins** — Access to `crypto`, `Buffer`, and other standard modules
|
||||
- **Environment variables** — Use `{{KEY}}` syntax to inject secrets
|
||||
|
||||
### Limitations
|
||||
|
||||
- **No npm packages** — External libraries like `axios` or `lodash` are not available. Use built-in APIs instead
|
||||
- **Parameters by name** — Schema parameters are available directly as variables (e.g., `city`), not via a `params` object
|
||||
|
||||
### Returning Results
|
||||
|
||||
Return a value from your code to send it back to the agent:
|
||||
|
||||
```javascript
|
||||
const result = await fetch(`https://api.example.com/data?q=${query}`);
|
||||
const data = await result.json();
|
||||
return data;
|
||||
```
|
||||
|
||||
The returned value becomes the tool output that the agent sees and can use in its response.
|
||||
|
||||
## Managing Custom Tools
|
||||
|
||||
From **Settings → Custom Tools** you can:
|
||||
|
||||
- **Search** tools by name, function name, or description
|
||||
- **Edit** any tool's schema or code
|
||||
- **Delete** tools that are no longer needed
|
||||
|
||||
<Callout type="warn">
|
||||
Deleting a custom tool removes it from all Agent blocks that reference it. Make sure no active workflows depend on the tool before deleting.
|
||||
</Callout>
|
||||
|
||||
## Permissions
|
||||
|
||||
| Action | Required Permission |
|
||||
|--------|-------------------|
|
||||
| View custom tools | **Read**, **Write**, or **Admin** |
|
||||
| Create or edit tools | **Write** or **Admin** |
|
||||
| Delete tools | **Admin** |
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "Can I use custom tools in standalone blocks (not agents)?", answer: "No. Custom tools are designed for use within Agent blocks, where the AI model decides when to call them. For deterministic tool execution, use the Function block instead." },
|
||||
{ question: "How do I pass API keys to my custom tool code?", answer: "Use environment variables with double curly brace syntax: {{MY_API_KEY}}. Create the environment variable in Settings → Secrets, and it will be injected at execution time without appearing in logs." },
|
||||
{ question: "Can I use external npm packages?", answer: "No. Custom tool code runs in a sandboxed environment with access to built-in Node.js modules and fetch(), but not external packages. For complex dependencies, consider calling an external API that wraps the functionality you need." },
|
||||
{ question: "What's the difference between custom tools and the Function block?", answer: "Custom tools are called by AI agents when they decide the tool is relevant — the agent chooses when to use it. Function blocks run deterministically at a fixed point in the workflow. Use custom tools for agent-driven actions and Function blocks for predictable data transformations." },
|
||||
{ question: "Are custom tools shared across the workspace?", answer: "Yes. Custom tools are workspace-scoped, so all workspace members can use them in their workflows." },
|
||||
]} />
|
||||
@@ -364,7 +364,7 @@ List assets tracked by a Dagster instance, optionally filtered by key prefix.
|
||||
| `apiKey` | string | No | Dagster+ API token \(leave blank for OSS / self-hosted\) |
|
||||
| `prefix` | string | No | Slash-delimited asset key prefix to filter by, e.g. "raw" or "raw/events" \(optional\) |
|
||||
| `cursor` | string | No | Asset key cursor from a previous response, for pagination \(optional\) |
|
||||
| `limit` | number | No | Maximum number of assets to return \(optional\) |
|
||||
| `limit` | number | No | Maximum number of assets to return per page \(default 100\) |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="dspy"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="elasticsearch"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="evernote"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
---
|
||||
title: File
|
||||
description: Read, fetch, write, and append files
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="file_v4"
|
||||
color="#40916C"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
The File Parser tool provides a powerful way to extract and process content from various file formats, making it easy to incorporate document data into your agent workflows. This tool supports multiple file formats and can handle files up to 200MB in size.
|
||||
|
||||
With the File Parser, you can:
|
||||
|
||||
- **Process multiple file formats**: Extract text from PDFs, CSVs, Word documents (DOCX), text files, and more
|
||||
- **Handle large files**: Process documents up to 200MB in size
|
||||
- **Parse files from URLs**: Directly extract content from files hosted online by providing their URLs
|
||||
- **Process multiple files at once**: Upload and parse several files in a single operation
|
||||
- **Extract structured data**: Maintain formatting and structure from the original documents when possible
|
||||
|
||||
The File Parser tool is particularly useful for scenarios where your agents need to work with document content, such as analyzing reports, extracting data from spreadsheets, or processing text from various document sources. It simplifies the process of making document content available to your agents, allowing them to work with information stored in files just as easily as with direct text input.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Read workspace files by picker or canonical ID, fetch and parse files from URLs with optional headers, write new workspace files, or append content to existing files.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `file_fetch`
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | file | First workspace file object \(read\) |
|
||||
| `files` | file[] | Workspace file objects \(read\) or fetched file objects \(fetch\) |
|
||||
| `combinedContent` | string | All fetched file contents merged into a single text string \(fetch\) |
|
||||
| `id` | string | File ID \(write\) |
|
||||
| `name` | string | File name \(write\) |
|
||||
| `size` | number | File size in bytes \(write\) |
|
||||
| `url` | string | URL to access the file \(write\) |
|
||||
|
||||
### `file_read`
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | file | First workspace file object \(read\) |
|
||||
| `files` | file[] | Workspace file objects \(read\) or fetched file objects \(fetch\) |
|
||||
| `combinedContent` | string | All fetched file contents merged into a single text string \(fetch\) |
|
||||
| `id` | string | File ID \(write\) |
|
||||
| `name` | string | File name \(write\) |
|
||||
| `size` | number | File size in bytes \(write\) |
|
||||
| `url` | string | URL to access the file \(write\) |
|
||||
|
||||
### `file_write`
|
||||
|
||||
Create a new workspace file. If a file with the same name already exists, a numeric suffix is added (e.g.,
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileName` | string | Yes | File name \(e.g., "data.csv"\). If a file with this name exists, a numeric suffix is added automatically. |
|
||||
| `content` | string | Yes | The text content to write to the file. |
|
||||
| `contentType` | string | No | MIME type for new files \(e.g., "text/plain"\). Auto-detected from file extension if omitted. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | File ID |
|
||||
| `name` | string | File name |
|
||||
| `size` | number | File size in bytes |
|
||||
| `url` | string | URL to access the file |
|
||||
|
||||
### `file_append`
|
||||
|
||||
Append content to an existing workspace file. The file must already exist. Content is added to the end of the file.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileName` | string | Yes | Name of an existing workspace file to append to. |
|
||||
| `content` | string | Yes | The text content to append to the file. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | File ID |
|
||||
| `name` | string | File name |
|
||||
| `size` | number | File size in bytes |
|
||||
| `url` | string | URL to access the file |
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="gitlab"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="gmail_v2"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_ads"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_bigquery"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_books"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_calendar_v2"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_contacts"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_docs"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_drive"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_forms"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_maps"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_meet"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_pagespeed"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_search"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_sheets_v2"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_slides_v2"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_tasks"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_translate"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="grafana"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="hunter"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
---
|
||||
title: Image Generator
|
||||
description: Generate images
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="image_generator_v2"
|
||||
color="#4D5FFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
The Image Generator block creates images from text prompts using leading image generation providers. Choose OpenAI for GPT Image models, Google Gemini for Nano Banana models, or Fal.ai for a multi-model catalog that includes Nano Banana, GPT Image, Seedream, FLUX, and Grok Imagine.
|
||||
|
||||
Use it to:
|
||||
|
||||
- **Generate production images**: Create polished visuals from workflow prompts
|
||||
- **Choose the right provider**: Route requests to OpenAI, Gemini, or Fal.ai based on model availability and cost
|
||||
- **Control output shape**: Set provider-specific size, aspect ratio, resolution, quality, background, and output format options
|
||||
- **Use advanced Fal.ai features**: Configure safety tolerance, safety checking, web search grounding, seeds, and thinking level when supported
|
||||
- **Pass generated files downstream**: Use the returned image file or URL in later workflow steps
|
||||
|
||||
In Sim, the Image Generator block lets agents create visual assets programmatically as part of automated workflows. This is useful for content creation, design mockups, product visuals, creative ideation, and any flow that needs generated imagery without a manual handoff.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Generate images using OpenAI GPT Image, Google Nano Banana, or Fal.ai image models.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `image_generate`
|
||||
|
||||
Generate images with OpenAI GPT Image, Google Nano Banana, or Fal.ai image models
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `provider` | string | Yes | Image generation provider: openai, gemini, or falai |
|
||||
| `apiKey` | string | Yes | Provider API key |
|
||||
| `model` | string | Yes | Provider model ID, such as gpt-image-1.5, gemini-3.1-flash-image-preview, or nano-banana-2 |
|
||||
| `prompt` | string | Yes | Text prompt describing the image to generate |
|
||||
| `size` | string | No | Provider-specific image size |
|
||||
| `aspectRatio` | string | No | Aspect ratio, such as auto, 1:1, 16:9, or 9:16 |
|
||||
| `resolution` | string | No | Provider-specific image resolution, such as 1K, 2K, 4K, 1k, or 2k |
|
||||
| `quality` | string | No | Provider-specific image quality |
|
||||
| `background` | string | No | Background setting when supported |
|
||||
| `outputFormat` | string | No | Output image format: png, jpeg, or webp where supported |
|
||||
| `moderation` | string | No | OpenAI moderation level: auto or low |
|
||||
| `safetyTolerance` | string | No | Fal.ai safety tolerance when supported |
|
||||
| `numImages` | number | No | Number of images to generate, subject to provider limits |
|
||||
| `seed` | number | No | Random seed when supported |
|
||||
| `enableSafetyChecker` | boolean | No | Enable the Fal.ai safety checker when supported |
|
||||
| `enableWebSearch` | boolean | No | Enable web search grounding when supported by the selected Fal.ai model |
|
||||
| `thinkingLevel` | string | No | Fal.ai thinking level when supported: minimal or high |
|
||||
| `pricing` | custom | No | No description |
|
||||
| `metadata` | string | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Generated image URL or identifier |
|
||||
| `image` | file | Generated image file |
|
||||
| `imageUrl` | string | Generated image URL |
|
||||
| `provider` | string | Provider used |
|
||||
| `model` | string | Model used |
|
||||
| `metadata` | json | Generation metadata |
|
||||
| ↳ `provider` | string | Provider used |
|
||||
| ↳ `model` | string | Model used |
|
||||
| ↳ `description` | string | Provider description |
|
||||
| ↳ `revisedPrompt` | string | Revised prompt |
|
||||
| ↳ `seed` | number | Seed used for generation |
|
||||
| ↳ `jobId` | string | Provider job ID |
|
||||
| ↳ `contentType` | string | Image MIME type |
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
title: IMAP Email
|
||||
description: Trigger workflows when new emails arrive via IMAP (works with any email provider)
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="imap"
|
||||
color="#6366F1"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
The IMAP Email trigger allows your Sim workflows to start automatically whenever a new email is received in any mailbox that supports the IMAP protocol. This works with Gmail, Outlook, Yahoo, and most other email providers.
|
||||
|
||||
With the IMAP trigger, you can:
|
||||
|
||||
- **Automate email processing**: Start workflows in real time when new messages arrive in your inbox.
|
||||
- **Filter by sender, subject, or folder**: Configure your trigger to react only to emails that match certain conditions.
|
||||
- **Extract and process attachments**: Automatically download and use file attachments in your automated flows.
|
||||
- **Parse and use email content**: Access the subject, sender, recipients, full body, and other metadata in downstream workflow steps.
|
||||
- **Integrate with any email provider**: Works with any service that provides standard IMAP access, without vendor lock-in.
|
||||
- **Trigger on unread, flagged, or custom criteria**: Set up advanced filters for the kinds of emails that start your workflows.
|
||||
|
||||
With Sim, the IMAP integration gives you the power to turn email into an actionable source of automation. Respond to customer inquiries, process notifications, kick off data pipelines, and more—directly from your email inbox, with no manual intervention.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Connect to any email server via IMAP protocol to trigger workflows when new emails are received. Supports Gmail, Outlook, Yahoo, and any other IMAP-compatible email provider.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="incidentio"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="intercom_v2"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="jira"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="jira_service_management"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="mailgun"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
---
|
||||
title: Memory
|
||||
description: Add memory store
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="memory"
|
||||
color="#F64F9E"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
The Memory tool enables your agents to store, retrieve, and manage conversation memories across workflows. It acts as a persistent memory store that agents can access to maintain conversation context, recall facts, or track actions over time.
|
||||
|
||||
With the Memory tool, you can:
|
||||
|
||||
- **Add new memories**: Store relevant information, events, or conversation history by saving agent or user messages into a structured memory database
|
||||
- **Retrieve memories**: Fetch specific memories or all memories tied to a conversation, helping agents recall previous interactions or facts
|
||||
- **Delete memories**: Remove outdated or incorrect memories from the database to maintain accurate context
|
||||
- **Append to existing conversations**: Update or expand on existing memory threads by appending new messages with the same conversation identifier
|
||||
|
||||
Sim’s Memory block is especially useful for building agents that require persistent state—helping them remember what was said earlier in a conversation, persist facts between tasks, or apply long-term history in decision-making. By integrating Memory, you enable richer, more contextual, and more dynamic workflows for your agents.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Memory into the workflow. Can add, get a memory, get all memories, and delete memories.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `memory_add`
|
||||
|
||||
Add a new memory to the database or append to existing memory with the same ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | No | Conversation identifier \(e.g., user-123, session-abc\). If a memory with this conversationId already exists, the new message will be appended to it. |
|
||||
| `id` | string | No | Legacy parameter for conversation identifier. Use conversationId instead. Provided for backwards compatibility. |
|
||||
| `role` | string | Yes | Role for agent memory \(user, assistant, or system\) |
|
||||
| `content` | string | Yes | Content for agent memory |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the memory was added successfully |
|
||||
| `memories` | array | Array of memory objects including the new or updated memory |
|
||||
| `error` | string | Error message if operation failed |
|
||||
|
||||
### `memory_get`
|
||||
|
||||
Retrieve memory by conversationId. Returns matching memories.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | No | Conversation identifier \(e.g., user-123, session-abc\). Returns memories for this conversation. |
|
||||
| `id` | string | No | Legacy parameter for conversation identifier. Use conversationId instead. Provided for backwards compatibility. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the memory was retrieved successfully |
|
||||
| `memories` | array | Array of memory objects with conversationId and data fields |
|
||||
| `message` | string | Success or error message |
|
||||
| `error` | string | Error message if operation failed |
|
||||
|
||||
### `memory_get_all`
|
||||
|
||||
Retrieve all memories from the database
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether all memories were retrieved successfully |
|
||||
| `memories` | array | Array of all memory objects with key, conversationId, and data fields |
|
||||
| `message` | string | Success or error message |
|
||||
| `error` | string | Error message if operation failed |
|
||||
|
||||
### `memory_delete`
|
||||
|
||||
Delete memories by conversationId.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | No | Conversation identifier \(e.g., user-123, session-abc\). Deletes all memories for this conversation. |
|
||||
| `id` | string | No | Legacy parameter for conversation identifier. Use conversationId instead. Provided for backwards compatibility. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the memory was deleted successfully |
|
||||
| `message` | string | Success or error message |
|
||||
| `error` | string | Error message if operation failed |
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"pages": [
|
||||
"index",
|
||||
"a2a",
|
||||
"agentmail",
|
||||
"agentphone",
|
||||
"agiloft",
|
||||
@@ -24,7 +23,6 @@
|
||||
"browser_use",
|
||||
"calcom",
|
||||
"calendly",
|
||||
"circleback",
|
||||
"clay",
|
||||
"clerk",
|
||||
"clickhouse",
|
||||
@@ -34,7 +32,6 @@
|
||||
"confluence",
|
||||
"crowdstrike",
|
||||
"cursor",
|
||||
"custom-tools",
|
||||
"dagster",
|
||||
"databricks",
|
||||
"datadog",
|
||||
@@ -55,7 +52,6 @@
|
||||
"exa",
|
||||
"extend",
|
||||
"fathom",
|
||||
"file",
|
||||
"findymail",
|
||||
"firecrawl",
|
||||
"fireflies",
|
||||
@@ -93,8 +89,6 @@
|
||||
"hunter",
|
||||
"iam",
|
||||
"identity_center",
|
||||
"image_generator",
|
||||
"imap",
|
||||
"incidentio",
|
||||
"infisical",
|
||||
"instantly",
|
||||
@@ -104,7 +98,6 @@
|
||||
"jira_service_management",
|
||||
"kalshi",
|
||||
"ketch",
|
||||
"knowledge",
|
||||
"langsmith",
|
||||
"launchdarkly",
|
||||
"lemlist",
|
||||
@@ -117,7 +110,6 @@
|
||||
"mailchimp",
|
||||
"mailgun",
|
||||
"mem0",
|
||||
"memory",
|
||||
"microsoft_ad",
|
||||
"microsoft_dataverse",
|
||||
"microsoft_excel",
|
||||
@@ -126,7 +118,6 @@
|
||||
"mistral_parse",
|
||||
"monday",
|
||||
"mongodb",
|
||||
"mysql",
|
||||
"neo4j",
|
||||
"new_relic",
|
||||
"notion",
|
||||
@@ -143,7 +134,6 @@
|
||||
"pinecone",
|
||||
"pipedrive",
|
||||
"polymarket",
|
||||
"postgresql",
|
||||
"posthog",
|
||||
"profound",
|
||||
"prospeo",
|
||||
@@ -164,42 +154,33 @@
|
||||
"salesforce",
|
||||
"sap_concur",
|
||||
"sap_s4hana",
|
||||
"search",
|
||||
"secrets_manager",
|
||||
"sendgrid",
|
||||
"sentry",
|
||||
"serper",
|
||||
"servicenow",
|
||||
"ses",
|
||||
"sftp",
|
||||
"sharepoint",
|
||||
"shopify",
|
||||
"similarweb",
|
||||
"sixtyfour",
|
||||
"slack",
|
||||
"smtp",
|
||||
"sqs",
|
||||
"ssh",
|
||||
"stagehand",
|
||||
"stripe",
|
||||
"sts",
|
||||
"stt",
|
||||
"supabase",
|
||||
"table",
|
||||
"tailscale",
|
||||
"tavily",
|
||||
"telegram",
|
||||
"textract",
|
||||
"tinybird",
|
||||
"translate",
|
||||
"trello",
|
||||
"tts",
|
||||
"twilio_sms",
|
||||
"twilio_voice",
|
||||
"typeform",
|
||||
"upstash",
|
||||
"vercel",
|
||||
"video_generator",
|
||||
"wealthbox",
|
||||
"webflow",
|
||||
"whatsapp",
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="microsoft_dataverse"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="microsoft_excel_v2"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="microsoft_planner"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="microsoft_teams"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="mongodb"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
---
|
||||
title: MySQL
|
||||
description: Connect to MySQL database
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="mysql"
|
||||
color="#E0E0E0"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
The [MySQL](https://www.mysql.com/) tool enables you to connect to any MySQL database and perform a wide range of database operations directly within your agentic workflows. With secure connection handling and flexible configuration, you can easily manage and interact with your data.
|
||||
|
||||
With the MySQL tool, you can:
|
||||
|
||||
- **Query data**: Execute SELECT queries to retrieve data from your MySQL tables using the `mysql_query` operation.
|
||||
- **Insert records**: Add new rows to your tables with the `mysql_insert` operation by specifying the table and data to insert.
|
||||
- **Update records**: Modify existing data in your tables using the `mysql_update` operation, providing the table, new data, and WHERE conditions.
|
||||
- **Delete records**: Remove rows from your tables with the `mysql_delete` operation, specifying the table and WHERE conditions.
|
||||
- **Execute raw SQL**: Run any custom SQL command using the `mysql_execute` operation for advanced use cases.
|
||||
|
||||
The MySQL tool is ideal for scenarios where your agents need to interact with structured data—such as automating reporting, syncing data between systems, or powering data-driven workflows. It streamlines database access, making it easy to read, write, and manage your MySQL data programmatically.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate MySQL into the workflow. Can query, insert, update, delete, and execute raw SQL.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `mysql_query`
|
||||
|
||||
Execute SELECT query on MySQL database
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | MySQL server hostname or IP address |
|
||||
| `port` | number | Yes | MySQL server port \(default: 3306\) |
|
||||
| `database` | string | Yes | Database name to connect to \(e.g., my_database\) |
|
||||
| `username` | string | Yes | Database username |
|
||||
| `password` | string | Yes | Database password |
|
||||
| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) |
|
||||
| `query` | string | Yes | SQL SELECT query to execute \(e.g., SELECT * FROM users WHERE active = 1\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `rows` | array | Array of rows returned from the query |
|
||||
| `rowCount` | number | Number of rows returned |
|
||||
|
||||
### `mysql_insert`
|
||||
|
||||
Insert new record into MySQL database
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | MySQL server hostname or IP address |
|
||||
| `port` | number | Yes | MySQL server port \(default: 3306\) |
|
||||
| `database` | string | Yes | Database name to connect to \(e.g., my_database\) |
|
||||
| `username` | string | Yes | Database username |
|
||||
| `password` | string | Yes | Database password |
|
||||
| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) |
|
||||
| `table` | string | Yes | Table name to insert into \(e.g., users, orders\) |
|
||||
| `data` | object | Yes | Data to insert as key-value pairs |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `rows` | array | Array of inserted rows |
|
||||
| `rowCount` | number | Number of rows inserted |
|
||||
|
||||
### `mysql_update`
|
||||
|
||||
Update existing records in MySQL database
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | MySQL server hostname or IP address |
|
||||
| `port` | number | Yes | MySQL server port \(default: 3306\) |
|
||||
| `database` | string | Yes | Database name to connect to \(e.g., my_database\) |
|
||||
| `username` | string | Yes | Database username |
|
||||
| `password` | string | Yes | Database password |
|
||||
| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) |
|
||||
| `table` | string | Yes | Table name to update \(e.g., users, orders\) |
|
||||
| `data` | object | Yes | Data to update as key-value pairs |
|
||||
| `where` | string | Yes | WHERE clause condition \(without WHERE keyword\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `rows` | array | Array of updated rows |
|
||||
| `rowCount` | number | Number of rows updated |
|
||||
|
||||
### `mysql_delete`
|
||||
|
||||
Delete records from MySQL database
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | MySQL server hostname or IP address |
|
||||
| `port` | number | Yes | MySQL server port \(default: 3306\) |
|
||||
| `database` | string | Yes | Database name to connect to \(e.g., my_database\) |
|
||||
| `username` | string | Yes | Database username |
|
||||
| `password` | string | Yes | Database password |
|
||||
| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) |
|
||||
| `table` | string | Yes | Table name to delete from \(e.g., users, orders\) |
|
||||
| `where` | string | Yes | WHERE clause condition \(without WHERE keyword\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `rows` | array | Array of deleted rows |
|
||||
| `rowCount` | number | Number of rows deleted |
|
||||
|
||||
### `mysql_execute`
|
||||
|
||||
Execute raw SQL query on MySQL database
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | MySQL server hostname or IP address |
|
||||
| `port` | number | Yes | MySQL server port \(default: 3306\) |
|
||||
| `database` | string | Yes | Database name to connect to \(e.g., my_database\) |
|
||||
| `username` | string | Yes | Database username |
|
||||
| `password` | string | Yes | Database password |
|
||||
| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) |
|
||||
| `query` | string | Yes | Raw SQL query to execute \(e.g., CREATE TABLE users \(id INT PRIMARY KEY, name VARCHAR\(255\)\)\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `rows` | array | Array of rows returned from the query |
|
||||
| `rowCount` | number | Number of rows affected |
|
||||
|
||||
### `mysql_introspect`
|
||||
|
||||
Introspect MySQL database schema to retrieve table structures, columns, and relationships
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | MySQL server hostname or IP address |
|
||||
| `port` | number | Yes | MySQL server port \(default: 3306\) |
|
||||
| `database` | string | Yes | Database name to connect to \(e.g., my_database\) |
|
||||
| `username` | string | Yes | Database username |
|
||||
| `password` | string | Yes | Database password |
|
||||
| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `tables` | array | Array of table schemas with columns, keys, and indexes |
|
||||
| `databases` | array | List of available databases on the server |
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="onedrive"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="onepassword"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="outlook"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="parallel_ai"
|
||||
color="#E0E0E0"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
---
|
||||
title: PostgreSQL
|
||||
description: Connect to PostgreSQL database
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="postgresql"
|
||||
color="#336791"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
The [PostgreSQL](https://www.postgresql.org/) tool enables you to connect to any PostgreSQL database and perform a wide range of database operations directly within your agentic workflows. With secure connection handling and flexible configuration, you can easily manage and interact with your data.
|
||||
|
||||
With the PostgreSQL tool, you can:
|
||||
|
||||
- **Query data**: Execute SELECT queries to retrieve data from your PostgreSQL tables using the `postgresql_query` operation.
|
||||
- **Insert records**: Add new rows to your tables with the `postgresql_insert` operation by specifying the table and data to insert.
|
||||
- **Update records**: Modify existing data in your tables using the `postgresql_update` operation, providing the table, new data, and WHERE conditions.
|
||||
- **Delete records**: Remove rows from your tables with the `postgresql_delete` operation, specifying the table and WHERE conditions.
|
||||
- **Execute raw SQL**: Run any custom SQL command using the `postgresql_execute` operation for advanced use cases.
|
||||
|
||||
The PostgreSQL tool is ideal for scenarios where your agents need to interact with structured data—such as automating reporting, syncing data between systems, or powering data-driven workflows. It streamlines database access, making it easy to read, write, and manage your PostgreSQL data programmatically.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate PostgreSQL into the workflow. Can query, insert, update, delete, and execute raw SQL.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `postgresql_query`
|
||||
|
||||
Execute a SELECT query on PostgreSQL database
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | PostgreSQL server hostname or IP address |
|
||||
| `port` | number | Yes | PostgreSQL server port \(default: 5432\) |
|
||||
| `database` | string | Yes | Database name to connect to |
|
||||
| `username` | string | Yes | Database username |
|
||||
| `password` | string | Yes | Database password |
|
||||
| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) |
|
||||
| `query` | string | Yes | SQL SELECT query to execute |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `rows` | array | Array of rows returned from the query |
|
||||
| `rowCount` | number | Number of rows returned |
|
||||
|
||||
### `postgresql_insert`
|
||||
|
||||
Insert data into PostgreSQL database
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | PostgreSQL server hostname or IP address |
|
||||
| `port` | number | Yes | PostgreSQL server port \(default: 5432\) |
|
||||
| `database` | string | Yes | Database name to connect to |
|
||||
| `username` | string | Yes | Database username |
|
||||
| `password` | string | Yes | Database password |
|
||||
| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) |
|
||||
| `table` | string | Yes | Table name to insert data into |
|
||||
| `data` | object | Yes | Data object to insert \(key-value pairs\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `rows` | array | Inserted data \(if RETURNING clause used\) |
|
||||
| `rowCount` | number | Number of rows inserted |
|
||||
|
||||
### `postgresql_update`
|
||||
|
||||
Update data in PostgreSQL database
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | PostgreSQL server hostname or IP address |
|
||||
| `port` | number | Yes | PostgreSQL server port \(default: 5432\) |
|
||||
| `database` | string | Yes | Database name to connect to |
|
||||
| `username` | string | Yes | Database username |
|
||||
| `password` | string | Yes | Database password |
|
||||
| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) |
|
||||
| `table` | string | Yes | Table name to update data in |
|
||||
| `data` | object | Yes | Data object with fields to update \(key-value pairs\) |
|
||||
| `where` | string | Yes | WHERE clause condition \(without WHERE keyword\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `rows` | array | Updated data \(if RETURNING clause used\) |
|
||||
| `rowCount` | number | Number of rows updated |
|
||||
|
||||
### `postgresql_delete`
|
||||
|
||||
Delete data from PostgreSQL database
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | PostgreSQL server hostname or IP address |
|
||||
| `port` | number | Yes | PostgreSQL server port \(default: 5432\) |
|
||||
| `database` | string | Yes | Database name to connect to |
|
||||
| `username` | string | Yes | Database username |
|
||||
| `password` | string | Yes | Database password |
|
||||
| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) |
|
||||
| `table` | string | Yes | Table name to delete data from |
|
||||
| `where` | string | Yes | WHERE clause condition \(without WHERE keyword\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `rows` | array | Deleted data \(if RETURNING clause used\) |
|
||||
| `rowCount` | number | Number of rows deleted |
|
||||
|
||||
### `postgresql_execute`
|
||||
|
||||
Execute raw SQL query on PostgreSQL database
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | PostgreSQL server hostname or IP address |
|
||||
| `port` | number | Yes | PostgreSQL server port \(default: 5432\) |
|
||||
| `database` | string | Yes | Database name to connect to |
|
||||
| `username` | string | Yes | Database username |
|
||||
| `password` | string | Yes | Database password |
|
||||
| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) |
|
||||
| `query` | string | Yes | Raw SQL query to execute |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `rows` | array | Array of rows returned from the query |
|
||||
| `rowCount` | number | Number of rows affected |
|
||||
|
||||
### `postgresql_introspect`
|
||||
|
||||
Introspect PostgreSQL database schema to retrieve table structures, columns, and relationships
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | PostgreSQL server hostname or IP address |
|
||||
| `port` | number | Yes | PostgreSQL server port \(default: 5432\) |
|
||||
| `database` | string | Yes | Database name to connect to |
|
||||
| `username` | string | Yes | Database username |
|
||||
| `password` | string | Yes | Database password |
|
||||
| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) |
|
||||
| `schema` | string | No | Schema to introspect \(default: public\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `tables` | array | Array of table schemas with columns, keys, and indexes |
|
||||
| ↳ `name` | string | Table name |
|
||||
| ↳ `schema` | string | Schema name \(e.g., public\) |
|
||||
| ↳ `columns` | array | Table columns |
|
||||
| ↳ `name` | string | Column name |
|
||||
| ↳ `type` | string | Data type \(e.g., integer, varchar, timestamp\) |
|
||||
| ↳ `nullable` | boolean | Whether the column allows NULL values |
|
||||
| ↳ `default` | string | Default value expression |
|
||||
| ↳ `isPrimaryKey` | boolean | Whether the column is part of the primary key |
|
||||
| ↳ `isForeignKey` | boolean | Whether the column is a foreign key |
|
||||
| ↳ `references` | object | Foreign key reference information |
|
||||
| ↳ `table` | string | Referenced table name |
|
||||
| ↳ `column` | string | Referenced column name |
|
||||
| ↳ `primaryKey` | array | Primary key column names |
|
||||
| ↳ `foreignKeys` | array | Foreign key constraints |
|
||||
| ↳ `column` | string | Local column name |
|
||||
| ↳ `referencesTable` | string | Referenced table name |
|
||||
| ↳ `referencesColumn` | string | Referenced column name |
|
||||
| ↳ `indexes` | array | Table indexes |
|
||||
| ↳ `name` | string | Index name |
|
||||
| ↳ `columns` | array | Columns included in the index |
|
||||
| ↳ `unique` | boolean | Whether the index enforces uniqueness |
|
||||
| `schemas` | array | List of available schemas in the database |
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user