Compare commits

..

174 Commits

Author SHA1 Message Date
Waleed
a78f3f9c2e fix(credential): fix service_account migration to avoid unsafe enum usage in same transaction (#3897) 2026-04-02 10:16:08 -07:00
Waleed
080a0a6123 feat(rippling): expand Rippling integration from to 86 tools, landing updates (#3886)
* feat(rippling): expand Rippling integration from 16 to 86 tools

* fix(rippling): add required constraints on name and data subBlocks for create operations

* fix(rippling): add subblock ID migrations for removed legacy fields

* fix(docs): add MANUAL-CONTENT markers to tailscale docs and regenerate

* fix(rippling): add missing response fields to tool transforms

Add fields found missing by validation agents:
- list_companies: physical_address
- list/get_supergroups: sub_group_type, read_only, parent, mutually_exclusive_key, cumulatively_exhaustive_default, include_terminated
- list/get/create/update_custom_object: native_category_id, managed_package_install_id, owner_id
- list/get/create/update_custom_app: icon, pages
- list/get/create/update_custom_object_field: managed_package_install_id

* fix(rippling): add missing block outputs and required data conditions

- Add 17 missing collection output keys (titles, workLocations, supergroups, etc.)
- Add delete/bulk/report output keys (deleted, results, report_id, etc.)
- Mark data subBlock required for create_business_partner, create_custom_app,
  and create_custom_object_field (all have required params via data JSON spread)
- Add optional: true to get_current_user work_email and company_id outputs

* fix(rippling): add missing supergroup fields and fix validation issues

- Add 5 missing supergroup fields (allow_non_employees, can_override_role_states, priority, is_invisible, ignore_prov_group_matching) to types, list, and get tools
- Fix ok fallback from true to false in supergroup inclusion/exclusion member update tools
- Fix truthy check to null check for description param in create_custom_object_field

* fix(rippling): add missing custom page fields and structured custom setting responses

- Add 5 missing CustomPage fields (components, actions, canvas_actions, variables, media) to types and all page tools
- Replace opaque data blob with structured field mapping in create/update custom setting transforms
- Fix secret_value type cast consistency in list_custom_settings

* fix(rippling): add missing response fields, fix truthy checks, and improve UX

- Add 9 missing Worker fields (location, gender, date_of_birth, race, ethnicity, citizenship, termination_details, custom_fields, country_fields)
- Add 5 missing User fields (name, emails, phone_numbers, addresses, photos)
- Add worker expandable field to GroupMember types and all 3 member list tools
- Add 5 optional params to trigger_report_run (includeObjectIds, includeTotalRows, formatDateFields, formatCurrencyFields, outputType)
- Fix truthy checks to null checks in create_department, create/update_work_location
- Fix customObjectId subBlock label to say "API Name" instead of "ID"

* update docs

* fix(rippling): fix truthy checks, add missing fields, and regenerate docs

- Replace all `if (params.x)` with `if (params.x != null)` across 30+ tool files to prevent empty string/false/zero suppression
- Add expandable `parent` and `department_hierarchy` fields to department tools
- Add expandable `parent` field to team tools
- Add `company` expandable field to get_current_user
- Add `addressType` param to create/update work location tools
- Fix `secret_value` output type from 'json' to 'string' in list_custom_settings
- Regenerate docs for all 86 tools from current definitions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rippling): add all remaining spec fields and regenerate docs

- Add 6 advanced params to create_custom_object_field: required, rqlDefinition,
  formulaAttrMetas, section, derivedFieldFormula, derivedAggregatedField
- Add 6 advanced params to update_custom_object_field: required, rqlDefinition,
  formulaAttrMetas, section, derivedFieldFormula, nameFieldDetails
- Add 4 record output fields to all custom object record tools: created_by,
  last_modified_by, owner_role, system_updated_at
- Add cursor param to get_current_user
- Add __meta response field to get_report_run
- Regenerate docs for all 86 tools

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rippling): align all tools with OpenAPI spec

- Add __meta to 14 GET-by-ID tools (MetaResponse pattern)
- Fix supergroup tools: add filter to list_supergroups, remove invalid
  cursor from 4 list endpoints, revert update members to PATCH with
  Operations body
- Fix query_custom_object_records: use query/limit/cursor body params,
  return cursor instead of nextLink
- Fix bulk_create: use rows_to_write per spec
- Fix create/update record body wrappers with externalId support
- Update types.ts param interfaces and block config mappings
- Add limit param mapping with Number() conversion in block config
- Regenerate docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rippling): address PR review comments — add dedicated subBlocks, fix data duplication, expand externalId condition

- Add dedicated apiName, businessPartnerGroupId, workerId, dataType subBlocks so required params are no longer hidden behind opaque data JSON
- Narrow `data: item` in custom object record tools to only include dynamic fields, avoiding duplication of enumerated fields
- Expand externalId subBlock condition to include create/update custom object record operations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rippling): remove data JSON required for ops with dedicated subBlocks

create_business_partner, create_custom_app, and create_custom_object_field
now have dedicated subBlocks for their required params, so the data JSON
field is supplementary (not required) for those operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rippling): use rest-destructuring for all custom object record data output

The spec uses additionalProperties for custom fields at the top level,
not a nested `data` sub-object. Use the same rest-destructuring pattern
across all 6 custom object record tools so `data` only contains dynamic
fields, not duplicates of enumerated standard fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rippling): make update_custom_object_record data param optional in type

Matches the tool's `required: false` — users may update only external_id
without changing data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rippling): add dedicated streetAddress subBlock for create_work_location

streetAddress is required by the tool but had no dedicated subBlock —
users had to include it in the data JSON. Now has its own required
subBlock matching the pattern used by all other required params.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rippling): add allOrNothing subBlock for bulk operations

The bulk create/update/delete tools accept an optional allOrNothing
boolean param, but it had no subBlock and no way to be passed through
the block UI. Added as an advanced-mode dropdown with boolean coercion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rippling): derive spreadOps from DATA_OPS to prevent divergence

Replace the hardcoded spreadOps array with a derivation from the
file-level DATA_OPS constant minus non-spread operations. This ensures
new create/update operations added to DATA_OPS automatically get
spread behavior without needing a second manual update.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* updated

* fix(rippling): replace generic JSON outputs with specific fields per API spec

- Extract file_url, expires_at, output_type from report run result blob
- Rename bulk create/update outputs to createdRecords/updatedRecords
- Fix list_custom_settings output key mismatch (settings → customSettings)
- Make data optional for update_custom_object_record in block
- Update block outputs to match new tool output fields

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix landing

* restore FF

* fix(rippling): add wandConfig, clean titles, and migrate legacy operation values

- Remove "(JSON)" suffix from all subBlock titles
- Add wandConfig with AI prompts for filter, expand, orderBy, query, data, records, and dataType fields
- Add OPERATION_VALUE_MIGRATIONS to migrate old operation values (list_employees → list_workers, etc.) preventing runtime errors on saved workflows

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rippling): fix grammar typos and revert unnecessary migration

- Fix "a object" → "an object" in update/delete object category descriptions
- Revert OPERATION_VALUE_MIGRATIONS (unnecessary for low-usage integration)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(landing): add interactive workspace preview tabs

Adds Tables, Files, Knowledge Base, Logs, and Scheduled Tasks preview
components to the landing hero, with sidebar nav items that switch to each view.

* test updates

* refactor(landing): clean up code quality issues in preview components

- Replace widthMultiplier with explicit width on PreviewColumn
- Replace key={i} with key={Icon.name} in connectorIcons
- Scope --c-active CSS variable to sidebar container, eliminating hardcoded #363636 duplication
- Replace '-  -  -' fallback with em dash
- Type onSelectNav as (id: SidebarView) removing the unsafe cast

* fix(landing): use stable index key in connectorIcons to avoid minification breakage

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 01:30:43 -07:00
Theodore Li
fc6fe193fa fix(credential) fix credential migration (#3896)
* fix(credential) fix credential migration

* Fix lint

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-04-02 04:29:01 -04:00
Theodore Li
bbc704fe05 feat(credentials) Add google service account support (#3828)
* feat(auth): allow google service account

* Add gmail support for google services

* Refresh creds on typing in impersonated email

* Switch to adding subblock impersonateUserEmail conditionally

* Directly pass subblock for impersonateUserEmail

* Fix lint

* Update documentation for google service accounts

* Fix lint

* Address comments

* Remove hardcoded scopes, remove orphaned migration script

* Simplify subblocks for google service account

* Fix lint

* Fix build error

* Fix documentation scopes listed for google service accounts

* Fix issue with credential selector, remove bigquery and ad support

* create credentialCondition

* Shift conditional render out of subblock

* Simplify sublock values

* Fix security message

* Handle tool service accounts

* Address bugbot

* Fix lint

* Fix manual credential input not showing impersonate

* Fix tests

* Allow watching param id and subblock ids

* Fix bad test

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-04-02 03:08:13 -04:00
Theodore Li
c016537564 fix(blog): Fix blog not loading (#3895)
* Fix blog not loading

* Use emcn icon

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-04-02 00:53:31 -04:00
Waleed
4c94f3cf78 improvement(providers): audit and update all provider model definitions (#3893)
* improvement(providers): audit and update all provider model definitions

* fix(providers): add maxOutputTokens to azure/o3 and azure/o4-mini

* fix(providers): move maxOutputTokens inside capabilities for azure models
2026-04-01 19:32:16 -07:00
Vikhyath Mondreti
27a11a269d improvement(workflow): seed start block on server side (#3890)
* improvement(workflow): seed start block on server side

* add creating state machine for optimistic switch

* fix worksapce switch

* address comments

* address error handling at correct level
2026-04-01 19:04:34 -07:00
Waleed
2c174ca4f6 feat(landing): added models pages (#3888)
* feat(landing): added models pages

* fix(models): address PR review feedback

Correct model structured-data price bounds, remove dead code in the models catalog helpers, and harden OG font loading with graceful fallbacks.

Made-with: Cursor

* relative imports, build fix

* lint

* fix(models): remove dead og-utils exports, fix formatTokenCount null guard
2026-04-01 18:23:35 -07:00
Waleed
ac831b85b2 chore(bun): update bunfig.toml (#3889)
* chore(bun): update bunfig.toml

* outdated bun lock

* chore(deps): downgrade @aws-sdk/client-secrets-manager to 3.940.0
2026-04-01 17:21:00 -07:00
Waleed
8527ae5d3b feat(providers): server-side credential hiding for Azure and Bedrock (#3884)
* fix: allow Bedrock provider to use AWS SDK default credential chain

Remove hard requirement for explicit AWS credentials in Bedrock provider.
When access key and secret key are not provided, the AWS SDK automatically
falls back to its default credential chain (env vars, instance profile,
ECS task role, EKS IRSA, SSO).

Closes #3694

Signed-off-by: majiayu000 <1835304752@qq.com>

* fix: add partial credential guard for Bedrock provider

Reject configurations where only one of bedrockAccessKeyId or
bedrockSecretKey is provided, preventing silent fallback to the
default credential chain with a potentially different identity.

Add tests covering all credential configuration scenarios.

Signed-off-by: majiayu000 <1835304752@qq.com>

* fix: clean up bedrock test lint and dead code

Remove unused config parameter and dead _lastConfig assignment
from mock factory. Break long mockReturnValue chain to satisfy
biome line-length rule.

Signed-off-by: majiayu000 <1835304752@qq.com>

* fix: address greptile review feedback on PR #3708

Use BedrockRuntimeClientConfig from SDK instead of inline type.
Add default return value for prepareToolsWithUsageControl mock.

Signed-off-by: majiayu000 <1835304752@qq.com>

* feat(providers): server-side credential hiding for Azure and Bedrock

* fix(providers): revert Bedrock credential fields to required with original placeholders

* fix(blocks): add hideWhenEnvSet to getProviderCredentialSubBlocks for Azure and Bedrock

* fix(agent): use getProviderCredentialSubBlocks() instead of duplicating credential subblocks

* fix(blocks): consolidate Vertex credential into shared factory with basic/advanced mode

* fix(types): resolve pre-existing TypeScript errors across auth, secrets, and copilot

* lint

* improvement(blocks): make Vertex AI project ID a password field

* fix(blocks): preserve vertexCredential subblock ID for backwards compatibility

* fix(blocks): follow canonicalParamId pattern correctly for vertex credential subblocks

* fix(blocks): keep vertexCredential subblock ID stable to preserve saved workflow state

* fix(blocks): add canonicalParamId to vertexCredential basic subblock to complete the swap pair

* fix types

* more types

---------

Signed-off-by: majiayu000 <1835304752@qq.com>
Co-authored-by: majiayu000 <1835304752@qq.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
2026-04-01 16:27:54 -07:00
Waleed
076c835ba2 improvement(credentials): consolidate OAuth modals and auto-fill credential name (#3887)
* improvement(credentials): consolidate OAuth modals and auto-fill credential name

* fix(credentials): context-aware subtitle for KB vs workflow
2026-04-01 15:48:49 -07:00
Vikhyath Mondreti
df6ceb61a4 fix(envvar): remove dead env var 2026-04-01 14:01:13 -07:00
Vikhyath Mondreti
2ede12aa0e fix(cost): worker crash incremenental case (#3885) 2026-04-01 11:42:19 -07:00
Waleed
42fb434354 fix(encryption): specify authTagLength on all AES-GCM cipher/decipher calls (#3883)
* fix: specify authTagLength in AES-GCM decipheriv calls

Fixes missing authTagLength parameter in createDecipheriv calls using
AES-256-GCM mode. Without explicit tag length specification, the
application may be tricked into accepting shorter authentication tags,
potentially allowing ciphertext spoofing.

CWE-310: Cryptographic Issues (gcm-no-tag-length)

* fix: specify authTagLength on createCipheriv calls for AES-GCM consistency

Complements #3881 by adding explicit authTagLength: 16 to the encrypt
side as well, ensuring both cipher and decipher specify the tag length.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: clean up crypto modules

- Fix error: any → error: unknown with proper type guard in encryption.ts
- Eliminate duplicate iv.toString('hex') calls in both encrypt functions
- Remove redundant string split in decryptApiKey (was splitting twice)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* new turborepo version

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Lakee Sivaraya <71339072+lakeesiv@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com>
Co-authored-by: NLmejiro <kuroda.k1021@gmail.com>
2026-04-01 10:46:58 -07:00
Waleed
dcebe3ae97 improvement(triggers): add tags to all trigger.dev task invocations (#3878)
* improvement(triggers): add tags to all trigger.dev task invocations

* fix(triggers): prefix unused type param in buildTags

* fix(triggers): remove unused type param from buildTags
2026-03-31 19:52:33 -07:00
Waleed
e39c534ee3 feat(providers): add Fireworks AI provider integration (#3873)
* feat(providers): add Fireworks AI provider integration

* fix(providers): remove unused logger and dead modelInfo from fireworks

* lint

* feat(providers): add Fireworks BYOK support and official icon

* fix(providers): add workspace membership check and remove shared fetch cache for fireworks models
2026-03-31 19:22:04 -07:00
Vikhyath Mondreti
b95a0491a0 fix(kb): chunking config persistence (#3877)
* fix(kb): persist chunking config correctly

* fix kb config as sot

* remove dead code

* fix doc req bodies

* add defaults for async for legacy docs
2026-03-31 19:16:23 -07:00
Waleed
a79c8a75ce fix(chat): align floating chat send button colors with home/mothership chat (#3876) 2026-03-31 18:11:02 -07:00
Vikhyath Mondreti
282ec8c58c fix(reorder): drag and drop hook (#3874)
* fix(reorder): drag and drop hook

* fix custom tool dropdown color

* fix mcp server url change propagation
2026-03-31 17:33:08 -07:00
Waleed
e45fbe0184 improvement(attio): validate integration, fix event bug, add missing tool and triggers (#3872)
* improvement(attio): validate integration, fix event bug, add missing tool and triggers

* fix(attio): wire new trigger extractors into dispatcher, trim targetUrl

Add extractAttioListData and extractAttioWorkspaceMemberData dispatch
branches in utils.server.ts so the four new triggers return correct
outputs instead of falling through to generic extraction.

Also add missing .trim() on targetUrl in update_webhook.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 16:44:49 -07:00
Waleed
512558dcb3 feat(launchdarkly): add LaunchDarkly integration for feature flag management (#3870)
* feat(launchdarkly): add LaunchDarkly integration for feature flag management

* fix(launchdarkly): guard empty instructions array, trim apiKey in auth header

* lint
2026-03-31 16:40:06 -07:00
Waleed
35411e465e feat(models): add gpt-5.4-mini and gpt-5.4-nano (#3871) 2026-03-31 16:32:24 -07:00
Waleed
72e28baa07 feat(extend): add Extend AI document processing integration (#3869)
* feat(extend): add Extend AI document processing integration

* fix(extend): cast json response to fix type error

* fix(extend): correct API request body structure per Extend docs

* fix(extend): address PR review comments

* fix(extend): sync integrations.json bgColor to #000000

* lint
2026-03-31 16:26:34 -07:00
Waleed
d99dd86bf2 feat(tailscale): add Tailscale integration with 20 API operations (#3868)
* feat(tailscale): add Tailscale integration with 20 API operations

* fix(tailscale): fix transformResponse signatures and block output types

* fix(tailscale): safe response.json() pattern, trim apiKey, guard expirySeconds
2026-03-31 16:26:17 -07:00
Waleed
7898e5d75f improvement(workflows): replace Zustand workflow sync with React Query as single source of truth (#3860)
* improvement(workflows): replace Zustand workflow sync with React Query as single source of truth

* fix(workflows): address PR review feedback — sandbox execution, hydration deadlock, test mock, copy casing

* lint

* improvement(workflows): adopt skipToken over enabled+as-string for type-safe conditional queries

* improvement(workflows): remove dead complexity, fix mutation edge cases

- Throw on state PUT failure in useCreateWorkflow instead of swallowing
- Use Map for O(1) lookups in duplicate/export loops (3 hooks)
- Broaden invalidation scope in update/delete mutations to lists()
- Switch workflow-block to useWorkflowMap for direct ID lookup
- Consolidate use-workflow-operations to single useWorkflowMap hook
- Remove workspace transition guard (sync body, unreachable timeout)
- Make switchToWorkspace synchronous (remove async/try-catch/finally)

* fix(workflows): resolve cold-start deadlock on direct URL navigation

loadWorkflowState used hydration.workspaceId (null on cold start) to
look up the RQ cache, causing "Workflow not found" even when the
workflow exists in the DB. Now falls back to getWorkspaceIdFromUrl()
and skips the cache guard when the cache is empty (letting the API
fetch proceed).

Also removes the redundant isRegistryReady guard in workflow.tsx that
blocked setActiveWorkflow when hydration.workspaceId was null.

* fix(ui): prevent flash of empty state while workflows query is pending

Dashboard and EmbeddedWorkflow checked workflow list length before
the RQ query resolved, briefly showing "No workflows" or "Workflow
not found" on initial load. Now gates on isPending first.

* fix(workflows): address PR review — await description update, revert state PUT throw

- api-info-modal: use mutateAsync for description update so errors
  are caught by the surrounding try/catch instead of silently swallowed
- useCreateWorkflow: revert state PUT to log-only — the workflow is
  already created in the DB, throwing rolls back the optimistic entry
  and makes it appear the creation failed when it actually succeeded

* move folders over to react query native, restructure passage of data

* pass signal correctly

* fix types

* fix workspace id

* address comment

* soft deletion accuring

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
2026-03-31 15:49:16 -07:00
Waleed
df62502903 feat(infra): add dev environment support (#3867)
* feat(infra): add dev environment support

* fix(ci): push :dev ECR tag when building from dev branch

* fix(feature-flags): simplify isHosted subdomain check

* fix(ci,feature-flags): guard URL parse, fix dev AWS creds in images.yml
2026-03-31 15:36:57 -07:00
Waleed
1a2aa6949e feat(secrets-manager): add AWS Secrets Manager integration (#3866)
* feat(secrets-manager): add AWS Secrets Manager integration

* fix(secrets-manager): address PR review feedback

- Conditional delete message based on forceDelete flag
- Add binary secret detection in getSecretValue

* fix(secrets-manager): handle boolean forceDelete and validate numeric inputs

- Accept both string 'true' and boolean true for forceDelete
- Guard parseInt results with isNaN check for maxResults and recoveryWindowInDays
2026-03-31 15:26:03 -07:00
Waleed
4544fd4519 improvement(ui): fix nav loading flash, skeleton mismatches, and React anti-patterns across resource pages (#3864)
* improvement(ui): fix nav loading flash, skeleton mismatches, and React anti-patterns across resource pages

- Convert knowledge, files, tables, scheduled-tasks, and home page.tsx files from async server components to simple client re-exports, eliminating the loading.tsx flash on every navigation
- Add client-side permission redirects (usePermissionConfig) to knowledge, files, and tables components to replace server-side checks
- Fix knowledge loading.tsx skeleton column count (6→7) and tables loading.tsx (remove phantom checkbox column)
- Fix connector document live updates: use isConnectorSyncingOrPending instead of status === 'syncing' so polling activates immediately after connector creation
- Remove dead chunk-switch useEffect in ChunkEditor (redundant with key prop remount)
- Replace useState+useEffect debounce with useDebounce hook in document.tsx
- Replace useRef+useEffect URL init with lazy useState initializers in document.tsx and logs.tsx
- Make handleToggleEnabled optimistic in document.tsx (cache first, onError rollback)
- Replace mutate+new Promise wrapper with mutateAsync+try/catch in base.tsx
- Fix schedule-modal.tsx: replace 15-setter useEffect with useState lazy initializers + key prop remount; wrap parseCronToScheduleType in useMemo
- Fix logs search: eliminate mount-only useEffect with eslint-disable by passing initialQuery to useSearchState; parse query once via shared initialParsed state
- Add useWorkspaceFileRecord hook to workspace-files.ts; refactor FileViewer to self-fetch
- Fix value: any → value: string in useTagSelection and collaborativeSetTagSelection
- Fix knowledge-tag-filters.tsx: pass '' instead of null when filters are cleared (type safety)

* fix(kb): use active scope in useWorkspaceFileRecord to share cache with useWorkspaceFiles

* fix(logs,kb,tasks): lazy-init useRef for URL param, add cold-path docs to useWorkspaceFileRecord, document key remount requirement in ScheduleModal

* fix(files): redirect to files list when file record not found in viewer

* revert(files): remove useEffect redirect from file-viewer, keep simple null return

* fix(scheduled-tasks): correct useMemo dep from schedule?.cronExpression to schedule
2026-03-31 11:56:58 -07:00
Geonwoo Kim
019630bdc8 fix(vllm): pass env.VLLM_API_KEY to chat requests (#3865) 2026-03-31 10:33:44 -07:00
Vikhyath Mondreti
90f592797a fix(file): use file-upload subblock (#3862)
* fix(file): use file-upload subblock

* fix preview + logs url for notifs

* fix color for profound

* remove canonical param from desc
2026-03-30 21:29:01 -07:00
Waleed
d091441e39 feat(logs): add copy link and deep-link support for log entries (#3863)
* feat(logs): add copy link and deep link support for log entries

* fix(logs): move Link icon to emcn and handle clipboard rejections

* feat(notifications): use executionId deep-link for View Log URLs

Switch buildLogUrl from ?search= to ?executionId= so email and Slack
'View Log' buttons open the logs page with the specific execution
auto-selected and the details panel expanded.
2026-03-30 21:17:58 -07:00
Waleed
7d4dd26760 fix(knowledge): fix document processing stuck in processing state (#3857)
* fix(knowledge): fix document processing stuck in processing state

* fix(knowledge): use Promise.allSettled for document dispatch and fix Copilot OAuth context

- Change Promise.all to Promise.allSettled in processDocumentsWithQueue so
  one failed dispatch doesn't abort the entire batch
- Add writeOAuthReturnContext before showing LazyOAuthRequiredModal from
  Copilot tools so useOAuthReturnForWorkflow can handle the return
- Add consumeOAuthReturnContext on modal close to clean up stale context

* fix(knowledge): fix type error in useCredentialRefreshTriggers call

Pass empty string instead of undefined for connectorProviderId fallback
to match the hook's string parameter type.

* upgrade turbo

* fix(knowledge): fix type error in connectors-section useCredentialRefreshTriggers call

Same string narrowing fix as add-connector-modal — pass empty string
fallback for providerId.
2026-03-30 20:35:08 -07:00
Vikhyath Mondreti
0abeac77e1 improvement(platform): standardize perms, audit logging, lifecycle across admin, copilot, ui actions (#3858)
* improvement(platform): standardize perms, audit logging, lifecycle mgmt across admin, copilot, ui actions

* address comments

* improve error codes

* address bugbot comments

* fix test
2026-03-30 20:25:38 -07:00
Waleed
e9c94fa462 feat(logs): add copy link and deep link support for log entries (#3855)
* feat(logs): add copy link and deep link support for log entries

* fix(logs): fetch next page when deep linked log is beyond initial page

* fix(logs): move Link icon to emcn and handle clipboard rejections

* fix(logs): track isFetching reactively and drop empty-list early-return

- Remove  guard that prevented clearing the
  pending ref when filters return no results
- Use  directly in the condition and add it to
  the effect deps so the effect re-triggers after a background refetch

* fix(logs): guard deep-link ref clear until query has succeeded

Only clear pendingExecutionIdRef when the query status is 'success',
preventing premature clearing before the initial fetch completes.
On mount, the query is disabled (isInitialized.current starts false),
so hasNextPage is false but no data has loaded yet — the ref was being
cleared in the same effect pass that set it.

* fix(logs): guard fetchNextPage call until query has succeeded

Add logsQuery.status === 'success' to the fetchNextPage branch so it
mirrors the clear branch. On mount the query is disabled (isFetching is
false, status is pending), causing the effect to call fetchNextPage()
before the query is initialized — now both branches require success.
2026-03-30 18:42:27 -07:00
Waleed
72eea64bf6 improvement(tour): align product tour tooltip styling with emcn and fix spotlight overflow (#3854) 2026-03-30 16:59:53 -07:00
Waleed
27460f847c fix(atlassian): harden cloud ID resolution for Confluence and Jira (#3853) 2026-03-30 16:54:45 -07:00
Vikhyath Mondreti
c7643198dc fix(mothership): hang condition (#3852) 2026-03-30 16:47:54 -07:00
Waleed
e5aef6184a feat(profound): add Profound AI visibility and analytics integration (#3849)
* feat(profound): add Profound AI visibility and analytics integration

* fix(profound): fix import ordering and JSON formatting for CI lint

* fix(profound): gate metrics mapping on current operation to prevent stale overrides

* fix(profound): guard JSON.parse on filters, fix offset=0 falsy check, remove duplicate prompt_answers in FILTER_OPS

* lint

* fix(docs): fix import ordering and trailing newline for docs lint

* fix(scripts): sort generated imports to match Biome's organizeImports order

* fix(profound): use != null checks for limit param across all tools

* fix(profound): flatten block output type to 'json' to pass block validation test

* fix(profound): remove invalid 'required' field from block inputs (not part of ParamConfig)

* fix(profound): rename tool files from kebab-case to snake_case for docs generator compatibility

* lint

* fix(docs): let biome auto-fix import order, revert custom sort in generator

* fix(landing): fix import order in sim icon-mapping via biome

* fix(scripts): match Biome's exact import sort order in docs generator

* fix(generate-docs): produce Biome-compatible JSON output

The generator wrote multi-line arrays for short string arrays (like tags)
and omitted trailing newlines, causing Biome format check failures in CI.
Post-process integrations.json to collapse short arrays onto single lines
and add trailing newlines to both integrations.json and meta.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 16:30:06 -07:00
Waleed
4ae5b1b620 improvement(workflow): use DOM hit-testing for edge drop-on-block detection (#3851) 2026-03-30 16:20:30 -07:00
Waleed
5c334874eb fix(auth): use standard 'Unauthorized' error in hybrid auth responses (#3850) 2026-03-30 16:11:04 -07:00
Theodore Li
d3d58a9615 Feat/improved logging (#3833)
* feat(logs): add additional metadata for workflow execution logs

* Revert "Feat(logs) upgrade mothership chat messages to error (#3772)"

This reverts commit 9d1b9763c5.

* Fix lint, address greptile comments

* improvement(sidebar): expand sidebar by hovering and clicking the edge (#3830)

* improvement(sidebar): expand sidebar by hovering and clicking the edge

* improvement(sidebar): add keyboard shortcuts for new workflow/task, center search modal, fix edge ARIA

* improvement(sidebar): use Tooltip.Shortcut for inline shortcut display

* fix(sidebar): change new workflow shortcut from Mod+Shift+W to Mod+Shift+P to avoid browser close-window conflict

* fix(hotkeys): fall back to event.code for international keyboard layout compatibility

* fix(sidebar): guard add-workflow shortcut with canEdit and isCreatingWorkflow checks

* feat(ui): handle image paste (#3826)

* feat(ui): handle image paste

* Fix lint

* Fix type error

---------

Co-authored-by: Theodore Li <theo@sim.ai>

* feat(files): interactive markdown checkbox toggling in preview (#3829)

* feat(files): interactive markdown checkbox toggling in preview

* fix(files): handle ordered-list checkboxes and fix index drift

* lint

* fix(files): remove counter offset that prevented checkbox toggling

* fix(files): apply task-list styling to ordered lists too

* fix(files): render single pass when interactive to avoid index drift

* fix(files): move useMemo above conditional return to fix Rules of Hooks

* fix(files): pass content directly to preview when not streaming to avoid stale frame

* improvement(home): position @ mention popup at caret and fix icon consistency (#3831)

* improvement(home): position @ mention popup at caret and fix icon consistency

* fix(home): pin mirror div to document origin and guard button anchor

* chore(auth): restore hybrid.ts to staging

* improvement(ui): sidebar (#3832)

* Fix logger tests

* Add metadata to mothership logs

---------

Co-authored-by: Theodore Li <theo@sim.ai>
Co-authored-by: Waleed <walif6@gmail.com>
Co-authored-by: Theodore Li <theo@sim.ai>
2026-03-30 19:02:17 -04:00
Waleed
1d59eca90a fix(analytics): use getBaseDomain for Profound host field (#3848)
request.url resolves to internal ALB hostname on ECS, not the public domain
2026-03-30 15:36:41 -07:00
Theodore Li
e1359b09d6 feat(block) add block write and append operations (#3665)
* Add file write and delete operations

* Add file block write operation

* Fix lint

* Allow loop-in-loop workflow edits

* Fix type error

* Remove file id input, output link correctly

* Add append tool

* fix lint

* Address feedback

* Handle writing to same file name gracefully

* Removed  mime type from append block

* Add lock for file append operation

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-03-30 18:08:51 -04:00
Waleed
35b3646330 fix(sidebar): cmd+click opens in new tab, shift+click for range select (#3846)
* fix(sidebar): cmd+click opens in new tab, shift+click for range select

* comment cleanup

* fix(sidebar): drop stale metaKey param from workflow and task selection hooks
2026-03-30 11:02:33 -07:00
Waleed
5c47ea58f8 chore(trigger): update @trigger.dev/sdk and @trigger.dev/build to 4.4.3 (#3843)
* chore(trigger): update @trigger.dev/sdk and @trigger.dev/build to 4.4.3

* fix(webhooks): execute non-polling webhooks inline when BullMQ is disabled
2026-03-30 09:27:15 -07:00
Vikhyath Mondreti
c4f4e6b48c fix(bullmq): disable temporarily (#3841) 2026-03-30 00:04:59 -07:00
Waleed
1728c370de improvement(landing): lighthouse performance and accessibility fixes (#3837)
* improvement(landing): lighthouse performance and accessibility fixes

* improvement(landing): extract FeatureToggleItem to deduplicate accessibility logic

* lint

* fix(landing): ensure explicit delay prop takes precedence over transition spread
2026-03-29 22:33:34 -07:00
Waleed
82e58a5082 fix(academy): hide academy pages until content is ready (#3839)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 22:18:13 -07:00
Waleed
336c065234 fix(viewer): image pan/zoom, sort fixes, sidebar dot fixes (#3836)
* feat(file-viewer): add pan and zoom to image preview

* fix(viewer): fix sort key mapping, disable load-more on sort, hide status dots when menu open

* fix(file-viewer): prevent scroll bleed and zoom button micro-pans

* fix(file-viewer): use exponential zoom formula to prevent zero/negative multiplier
2026-03-29 12:40:29 -07:00
Waleed
b3713642b2 feat(resources): add sort and filter to all resource list pages (#3834)
* improvement(tables): improve table filtering UX

- Replace popover filter with persistent inline panel below toolbar
- Add AND/OR toggle between filter rules (shown in Where label slot)
- Sync filter panel state from applied filter on open
- Show filter button active state when filter is applied or panel is open
- Use readable operator labels matching dropdown options
- Add Clear filters button (shown only when filter is active)
- Close filter panel when last rule is removed via X
- Fix empty gap rows appearing in filtered results by skipping position gap rendering when filter is active
- Add toggle mode to ResourceOptionsBar for inline panel pattern
- Memoize FilterRuleRow for perf, fix filterTags key collision, remove dead filterActiveCount prop

* fix(table-filter): use ref to stabilize handleRemove/handleApply callbacks

Reading rules via ref instead of closure eliminates rules from useCallback
dependency arrays, keeping callbacks stable across rule edits and preserving
the memo() benefit on FilterRuleRow.

* improvement(tables,kb): remove hacky patterns, fix KB filter popover width

- Remove non-TSDoc comment from table-filter (rulesRef pattern is self-evident)
- Simplify SearchSection: remove setState-during-render anti-pattern; controlled
  input binds directly to search.value/onChange (simpler and correct)
- Reduce KB filter popover from w-[320px] to w-[200px]; tag filter uses vertical
  layout so narrow width works; Status-only case is now appropriately compact

* feat(knowledge): add sort and filter to KB list page

Sort dropdown: name, documents, tokens, created, last updated — pre-sorted
externally before passing rows to Resource. Active sort highlights the Sort
button; clear resets to default (created desc).

Filter popover: filter by connector status (All / With connectors /
Without connectors). Active filter shown as a removable tag in the toolbar.

* feat(files): add sort and filter to files list page

* feat(scheduled-tasks): add sort and filter to scheduled tasks page

* fix(table-filter): use explicit close handler instead of toggle

* improvement(files,knowledge): replace manual debounce with useDebounce hook and use type guards for file filtering

* fix(resource): prevent popover from inheriting anchor min-width

* feat(tables): add sort to tables list page

* feat(knowledge): add content and owner filters to KB list

* feat(scheduled-tasks): add status and health filters

* feat(files): add size and uploaded-by filters to files list

* feat(tables): add row count, owner, and column type filters

* improvement(scheduled-tasks): use combobox filter panel matching logs UI style

* improvement(knowledge): use combobox filter panel matching logs UI style

* improvement(files): use combobox filter panel matching logs UI style

Replaces button-list filters with Combobox-based multi-select sections for file type, size, and uploaded-by filters, aligning the panel with the logs page filter UI.

* improvement(tables): use combobox filter panel matching logs UI style

* feat(settings): add sort to recently deleted page

Add a sort dropdown next to the search bar allowing users to sort by deletion date (default, newest first), name (A–Z), or type (A–Z).

* feat(logs): add sort to logs page

* improvement(knowledge): upgrade document list filter to combobox style

* fix(resources): fix missing imports, memoization, and stale refs across resource pages

* improvement(tables): remove column type filter

* fix(resources): fix filter/sort correctness issues from audit

* fix(chunks): add server-side sort to document chunks API

Chunk sort was previously done client-side on a single page of
server-paginated data, which only reordered the current page.
Now sort params (sortBy, sortOrder) flow through the full stack:
types → service → API route → query hook → useDocumentChunks → document.tsx.

* perf(resources): memoize filterContent JSX across all resource pages

Resource is wrapped in React.memo, so an unstable filterContent reference
on every parent re-render defeats the memo. Wrap filterContent in useMemo
with correct deps in all 6 pages (files, tables, scheduled-tasks, knowledge,
base, document).

* fix(resources): add missing sort options for all visible columns

Every column visible in a resource table should be sortable. Three pages
had visible columns with no sort support:
- files.tsx: add 'owner' sort (member name lookup)
- scheduled-tasks.tsx: add 'schedule' sort (localeCompare on description)
- knowledge.tsx: add 'connectors' (count) and 'owner' (member name) sorts

Also add 'members' to processedKBs deps in knowledge.tsx since owner
sort now reads member names inside the memo.

* whitelabeling updates, sidebar fixes, files bug

* increased type safety

* pr fixes
2026-03-28 23:31:54 -07:00
Waleed
b9b930bb63 feat(analytics): add Profound web traffic tracking (#3835)
* feat(analytics): add Profound web traffic tracking

* fix(analytics): address PR review — add endpoint check and document trade-offs

* chore(analytics): remove implementation comments

* fix(analytics): guard sendToProfound with try-catch and align check with isProfoundEnabled

* fix(analytics): strip sensitive query params and remove redundant guard

* chore(analytics): remove unnecessary query param filtering
2026-03-28 22:09:23 -07:00
Vikhyath Mondreti
f1ead2ed55 fix docker image build 2026-03-28 20:58:56 -07:00
Waleed
30377d775b improvement(ui): sidebar (#3832) 2026-03-28 15:24:07 -07:00
Waleed
d013132d0e improvement(home): position @ mention popup at caret and fix icon consistency (#3831)
* improvement(home): position @ mention popup at caret and fix icon consistency

* fix(home): pin mirror div to document origin and guard button anchor

* chore(auth): restore hybrid.ts to staging
2026-03-28 14:48:41 -07:00
Waleed
7b0ce8064a feat(files): interactive markdown checkbox toggling in preview (#3829)
* feat(files): interactive markdown checkbox toggling in preview

* fix(files): handle ordered-list checkboxes and fix index drift

* lint

* fix(files): remove counter offset that prevented checkbox toggling

* fix(files): apply task-list styling to ordered lists too

* fix(files): render single pass when interactive to avoid index drift

* fix(files): move useMemo above conditional return to fix Rules of Hooks

* fix(files): pass content directly to preview when not streaming to avoid stale frame
2026-03-28 14:26:29 -07:00
Theodore Li
0ea73263df feat(ui): handle image paste (#3826)
* feat(ui): handle image paste

* Fix lint

* Fix type error

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-03-28 17:15:33 -04:00
Waleed
edc502384b improvement(sidebar): expand sidebar by hovering and clicking the edge (#3830)
* improvement(sidebar): expand sidebar by hovering and clicking the edge

* improvement(sidebar): add keyboard shortcuts for new workflow/task, center search modal, fix edge ARIA

* improvement(sidebar): use Tooltip.Shortcut for inline shortcut display

* fix(sidebar): change new workflow shortcut from Mod+Shift+W to Mod+Shift+P to avoid browser close-window conflict

* fix(hotkeys): fall back to event.code for international keyboard layout compatibility

* fix(sidebar): guard add-workflow shortcut with canEdit and isCreatingWorkflow checks
2026-03-28 14:06:30 -07:00
Waleed
e2be99263c feat(academy): Sim Academy — interactive partner certification platform (#3824)
* fix(import): dedup workflow name (#3813)

* feat(concurrency): bullmq based concurrency control system (#3605)

* feat(concurrency): bullmq based queueing system

* fix bun lock

* remove manual execs off queues

* address comments

* fix legacy team limits

* cleanup enterprise typing code

* inline child triggers

* fix status check

* address more comments

* optimize reconciler scan

* remove dead code

* add to landing page

* Add load testing framework

* update bullmq

* fix

* fix headless path

---------

Co-authored-by: Theodore Li <teddy@zenobiapay.com>

* fix(linear): add default null for after cursor (#3814)

* fix(knowledge): reject non-alphanumeric file extensions from document names (#3816)

* fix(knowledge): reject non-alphanumeric file extensions from document names

* fix(knowledge): improve error message when extension is non-alphanumeric

* fix(security): SSRF, access control, and info disclosure (#3815)

* fix(security): scope copilot feedback GET endpoint to authenticated user

Add WHERE clause to filter feedback records by the authenticated user's
ID, preventing any authenticated user from reading all users' copilot
interactions, queries, and workflow YAML (IDOR / CWE-639).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(smtp): add SSRF validation and genericize network error messages

Prevent SSRF via user-controlled smtpHost by validating with
validateDatabaseHost before creating the nodemailer transporter.
Collapse distinct network error messages (ECONNREFUSED, ECONNRESET,
ETIMEDOUT) into a single generic message to prevent port-state leakage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): add SSRF validation to SFTP/SSH and access control to workspace invitations

Add `validateDatabaseHost` checks to SFTP and SSH connection utilities to
block connections to private/reserved IPs and localhost, matching the
existing pattern used by all database tools. Add authorization check to
the workspace invitation GET endpoint so only the invitee or a workspace
admin can view invitation details.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(smtp): restore SMTP response code handling for post-connection errors

SMTP 4xx/5xx response codes are application-level errors (invalid
recipient, mailbox full, server error) unrelated to the SSRF hardening
goal. Restore response code differentiation and logging to preserve
actionable user-facing error messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): use session email directly instead of extra DB query

Addresses PR review feedback — align with the workspace invitation
route pattern by using session.user.email instead of re-fetching
from the database.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* lint

* fix(auth): revert lint autofix that broke hasExternalApiCredentials return type

Biome auto-fixed `return auth !== null && auth.startsWith(...)` to
`return auth?.startsWith(...)` which returns `boolean | undefined`,
not `boolean`, causing a TypeScript build failure.

* fix(smtp): pin resolved IP to prevent DNS rebinding (TOCTOU)

Use the pre-resolved IP from validateDatabaseHost instead of the
original hostname when creating the nodemailer transporter. Set
servername to the original hostname to preserve TLS SNI validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(security): extract createPinnedLookup helper for DNS rebinding prevention

Extract reusable createPinnedLookup from secureFetchWithPinnedIP so
non-HTTP transports (SSH, SFTP, IMAP) can pin resolved IPs at the
socket level. SMTP route uses host+servername pinning instead since
nodemailer doesn't reliably pass lookup to both secure/plaintext paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): pin IMAP connections to validated resolved IP

Pass the resolved IP from validateDatabaseHost to ImapFlow as host,
with the original hostname as servername for TLS SNI verification.
Closes the DNS TOCTOU rebinding window.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* lint

* fix(auth): revert lint autofix on hasExternalApiCredentials return type

Also pin SFTP/SSH connections to validated resolved IP to prevent DNS rebinding.

* fix(security): short-circuit admin check when caller is invitee

Skip the hasWorkspaceAdminAccess DB query when the caller is already
the invitee, avoiding an unnecessary round-trip. Aligns with the org
invitation route pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix(worker): dockerfile + helm updates (#3818)

* fix(worker): dockerfile + helm updates

* address comments

* update dockerfile (#3819)

* fix dockerfile

* fix(security): pentest remediation — condition escaping, SSRF hardening, ReDoS protection (#3820)

* fix(executor): escape newline characters in condition expression strings

Unescaped newline/carriage-return characters in resolved string values
cause unterminated string literals in generated JS, crashing condition
evaluation with a SyntaxError.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): prevent ReDoS in guardrails regex validation

Add safe-regex2 to reject catastrophic backtracking patterns before
execution and cap input length at 10k characters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): SSRF localhost hardening and regex DoS protection

Block localhost/loopback URLs in hosted environments using isHosted flag
instead of allowHttp. Add safe-regex2 validation and input length limits
to regex guardrails to prevent catastrophic backtracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): validate regex syntax before safety check

Move new RegExp() before safe() so invalid patterns get a proper syntax
error instead of a misleading "catastrophic backtracking" message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): address PR review feedback

- Hoist isLocalhost && isHosted guard to single early-return before
  protocol checks, removing redundant duplicate block
- Move regex syntax validation (new RegExp) before safe-regex2 check
  so invalid patterns get proper syntax error instead of misleading
  "catastrophic backtracking" message

* fix(security): remove input length cap from regex validation

The 10k character cap would block legitimate guardrail checks on long
LLM outputs. Input length doesn't affect ReDoS risk — the safe-regex2
pattern check already prevents catastrophic backtracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tests): mock isHosted in input-validation and function-execute tests

Tests that assert self-hosted localhost behavior need isHosted=false,
which is not guaranteed in CI where NEXT_PUBLIC_APP_URL is set to the
hosted domain.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* improvement(worker): configuration defaults (#3821)

* improvement(worker): configuration defaults

* update readmes

* realtime curl import

* improvement(tour): remove auto-start, only trigger on explicit user action (#3823)

* fix(mcp): use correct modal for creating workflow MCP servers in deploy (#3822)

* fix(mcp): use correct modal for creating workflow MCP servers in deploy

* fix(mcp): show workflows field during loading and when empty

* mock course

* fix(db): use bigint for token counter columns in user_stats (#3755)

* mock course

* updates

* updated X handle for emir

* cleanup: audit and clean academy implementation

* fix(academy): add label to ValidationRule, fix quiz gating, simplify getRuleMessage

* cleanup: remove unnecessary comments across academy files

* refactor(academy): simplify abstractions and fix perf issues

* perf(academy): convert course detail page to server component with client island

* fix(academy): null-safe canAdvance, render exercise instructions, remove stale comments

* fix(academy): remove orphaned migration, fix getCourseById, clean up comments

- Delete 0181_academy_certificate.sql (orphaned duplicate not in journal)
- Add getCourseById() to content/index.ts; use it in certificates API
  (was using getCourse which searches by slug, not stable id)
- Remove JSX comments from catalog page
- Remove redundant `passed` recomputation in LessonQuiz

* chore(db): regenerate academy_certificate migration with drizzle-kit

* chore: include blog mdx and components changes

* fix(blog): correct cn import path

* fix(academy): constrain progress bar to max-w-3xl with proper padding

* feat(academy): show back-to-course button on first lesson

* fix(academy): force dark theme on all /academy routes

* content(academy): rewrite sim-foundations course with full 6-module curriculum

* fix(academy): correct edge handles, quiz explanation, and starter mock outputs

- Fix Exercise 2 initial edge handles: 'starter-1-source'/'agent-1-target' → 'source'/'target' (React Flow actual IDs)
- Fix M1-L4 Q4 quiz explanation: remove non-existent Ctrl/Cmd+D and Alt+drag shortcuts
- Add starter mock output to all exercises so run animation shows feedback on the first block

* refine(academy): fix inaccurate content and improve exercise clarity

- Fix Exercise 3: replace hardcoded <agent-1.content> (invalid UUID-based ref) with reference picker instructions
- Fix M4 Quiz Q5: Loop block (subflow container) is correct answer, not the Workflow block
- Fix M4 Quiz Q4: clarify fan-out vs Parallel block distinction in explanation
- Fix M4-L2 video description: accurately describe Loop and Parallel subflow blocks
- Fix M2 Quiz Q3: make response format question conceptual rather than syntax-specific
- Improve Exercise 4 branching instructions: clarify top=true / bottom=false output handles
- Improve Final Project instructions: step-by-step numbered flow

* fix(academy): remove double border on quiz question cards

* fix(academy): single scroll container on lesson pages — remove nested flex scroll

* fix(academy): remove min-h-screen from root layout — fixes double scrollbar on lesson pages

* fix(academy): use fixed inset-0 on lesson page to eliminate document-level scrollbar

* fix(academy): replace sr-only radio/checkbox inputs with buttons to prevent scroll-on-focus; restore layout min-h-screen

* improvement(academy): polish, security hardening, and certificate claim UI

- Replace raw localStorage with BrowserStorage utility in local-progress
- Pre-compute slug/id Maps in content/index for O(1) course lookups
- Move blockMap construction into edge_exists branch only in validation
- Extract navBtnClass constant and MetaRow/formatDate helpers in UI
- Add rate limiting, server-side completion verification, audit logging, and nanoid cert numbers to certificate issuance endpoint
- Add useIssueCertificate mutation hook with completedLessonIds
- Wire certificate claim UI into CourseProgress: sign-in prompt, claim button with loading state, and post-issuance view with link to certificate page
- Fix lesson page scroll container and quiz scroll-on-focus bug

* fix(academy): validate condition branch handles in edge_exists rules

- Add sourceHandle field to edge_exists ValidationRule type
- Check sourceHandle in validation.ts when specified
- Require both condition-if and condition-else branches to be connected in the branching and final project exercises

* fix(academy): address PR review — isHosted regression, stuck isExecuting, revoked cert 500, certificate SSR

- Restore env-var-based isHosted check (was hardcoded true, breaking self-hosted deployments)
- Fix isExecuting stuck at true when mock run fails validation — set isMockRunningRef immediately and reset both flags on early exit
- Fix revoked/expired certificate causing 500 — any existing record (not just active) now returns 409 instead of falling through to INSERT
- Convert certificate verification page from client component to server component — direct DB fetch, notFound() on missing cert, generateMetadata for SEO/social previews

* fix(auth): restore hybrid.ts from staging to fix CI type error

* fix(academy): mark video lessons complete on visit and fix sign-in path

* fix(academy): replace useEffect+setState with lazy useState initializer in CourseProgress

* fix(academy): reset exerciseComplete on lesson navigation, remove unused useAcademyCertificate hook

* fix(academy): useState for slug-change reset, cache() for cert page, handleMockRunRef for stale closure

* fix(academy): replace shadcn theme vars with explicit hex in LessonVideo fallback

* fix(academy): reset completedRef on exercise change, conditional verified badge, multi-select empty guard

* fix(academy): type safety fixes — null metadata fallbacks, returning() guard, exhaustive union, empty catch

* fix(academy): reset ExerciseView completed banner on nav; fix CourseProgress hydration mismatch

* fix(lightbox): guard effect body with isOpen to prevent spurious overflow reset

* fix(academy): reset LessonQuiz state on lesson change to prevent stale answers persisting

* fix(academy): course not-found metadata title; try-finally guard in mock run loop

* fix(academy): type safety, cert persistence, regex guard, mixed-lesson video, shorts support

- Derive AcademyCertificate from db $inferSelect to prevent schema drift
- Add useCourseCertificate query hook; GET /api/academy/certificates now accepts courseId for authenticated lookup
- Use useCourseCertificate in CourseProgress so certificate state survives page refresh
- Guard new RegExp(valuePattern) in validation.ts with try/catch; log warn on invalid pattern
- Add logger.warn for custom validation rules so content authors are alerted
- Add YouTube Shorts URL support to LessonVideo (youtube.com/shorts/VIDEO_ID)
- Fix mixed-lesson video gap: render videoUrl above quiz when mixed has quiz but no exercise
- Add academy-scoped not-found.tsx with link back to /academy

* fix(academy): reset hintIndex when exercise changes

* chore: remove ban-spam-accounts script (wrong branch)

* fix(academy): enforce availableBlocks in toolbar; fix mixed exercise+quiz rendering

- Add useSandboxBlockConstraints context; SandboxCanvasProvider provides exerciseConfig.availableBlocks so the toolbar only shows permitted block types. Empty array hides all blocks (configure-only exercises); non-null array restricts to listed types; triggers always hidden in sandbox.
- Fix mixed lesson with both exerciseConfig and quizConfig: exercise renders first, quiz reveals after exercise completes (sequential pedagogy). canAdvance now requires both exerciseComplete && quizComplete when both are present.

* chore(academy): remove extraneous inline comments

* fix(academy): blank mixed lesson, quiz canAdvance flag, empty-array valueNotEmpty

* prep for merge

* chore(db): regenerate academy certificate migration after staging merge

* fix(academy): disable auto-connect in sandbox mode

* fix(academy): render video in mixed lesson with no exercise or quiz

* fix(academy): mark mixed video-only lessons complete; handle cert insert race

* fix(canvas): add sandbox and embedded to nodes useMemo deps

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Lakee Sivaraya <71339072+lakeesiv@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com>
Co-authored-by: Theodore Li <teddy@zenobiapay.com>
2026-03-28 12:46:04 -07:00
Waleed
f6b461ad47 fix(readme): restore readme gifs (#3827)
The static GIFs referenced by README.md were removed in #3803.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 12:36:23 -07:00
Waleed
e4d35735b1 fix(knowledge): give users choice to keep or delete documents when removing connector (#3825)
* fix(knowledge): give users choice to keep or delete documents when removing connector

* refactor(knowledge): clean up connector delete and extract shared extension validator

- Extract `isAlphanumericExtension` helper to deduplicate regex across parser-extension.ts and validation.ts
- Extract `closeDeleteModal` callback to eliminate 4x scattered state resets
- Add archivedAt/deletedAt filters to UPDATE query in keep-docs delete path
- Parallelize storage file cleanup and tag definition cleanup with Promise.all
- Deduplicate URL construction in delete connector hook

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(knowledge): remove duplicate extension list from parser-extension

Use SUPPORTED_DOCUMENT_EXTENSIONS and isSupportedExtension from
validation.ts instead of maintaining a separate identical list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(db): change document.connectorId FK from cascade to set null

The cascade behavior meant deleting a connector would always delete
its documents, contradicting the "keep documents" option. With set null,
the database automatically nullifies connectorId when a connector is
removed, and we only need explicit deletion when the user opts in.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(db): add migration metadata for connectorId FK change

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(knowledge): fix connector delete test and use URL-safe searchParams

Use `new URL(request.url).searchParams` instead of `request.nextUrl.searchParams`
for compatibility with test mocks. Add missing `connectorType` to test fixture.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* spacing

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 12:04:26 -07:00
Waleed
b4064c57fb fix(mcp): use correct modal for creating workflow MCP servers in deploy (#3822)
* fix(mcp): use correct modal for creating workflow MCP servers in deploy

* fix(mcp): show workflows field during loading and when empty
2026-03-27 20:22:30 -07:00
Waleed
eac41ca105 improvement(tour): remove auto-start, only trigger on explicit user action (#3823) 2026-03-27 19:59:47 -07:00
Vikhyath Mondreti
d2c3c1c39e improvement(worker): configuration defaults (#3821)
* improvement(worker): configuration defaults

* update readmes

* realtime curl import
2026-03-27 19:54:56 -07:00
Waleed
8f3e864751 fix(security): pentest remediation — condition escaping, SSRF hardening, ReDoS protection (#3820)
* fix(executor): escape newline characters in condition expression strings

Unescaped newline/carriage-return characters in resolved string values
cause unterminated string literals in generated JS, crashing condition
evaluation with a SyntaxError.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): prevent ReDoS in guardrails regex validation

Add safe-regex2 to reject catastrophic backtracking patterns before
execution and cap input length at 10k characters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): SSRF localhost hardening and regex DoS protection

Block localhost/loopback URLs in hosted environments using isHosted flag
instead of allowHttp. Add safe-regex2 validation and input length limits
to regex guardrails to prevent catastrophic backtracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): validate regex syntax before safety check

Move new RegExp() before safe() so invalid patterns get a proper syntax
error instead of a misleading "catastrophic backtracking" message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): address PR review feedback

- Hoist isLocalhost && isHosted guard to single early-return before
  protocol checks, removing redundant duplicate block
- Move regex syntax validation (new RegExp) before safe-regex2 check
  so invalid patterns get proper syntax error instead of misleading
  "catastrophic backtracking" message

* fix(security): remove input length cap from regex validation

The 10k character cap would block legitimate guardrail checks on long
LLM outputs. Input length doesn't affect ReDoS risk — the safe-regex2
pattern check already prevents catastrophic backtracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tests): mock isHosted in input-validation and function-execute tests

Tests that assert self-hosted localhost behavior need isHosted=false,
which is not guaranteed in CI where NEXT_PUBLIC_APP_URL is set to the
hosted domain.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 19:54:00 -07:00
Vikhyath Mondreti
23c3072784 fix dockerfile 2026-03-27 19:11:02 -07:00
Vikhyath Mondreti
33fdb11396 update dockerfile (#3819) 2026-03-27 18:50:57 -07:00
Vikhyath Mondreti
21156dd54a fix(worker): dockerfile + helm updates (#3818)
* fix(worker): dockerfile + helm updates

* address comments
2026-03-27 18:28:36 -07:00
Waleed
c05e2e0fc8 fix(security): SSRF, access control, and info disclosure (#3815)
* fix(security): scope copilot feedback GET endpoint to authenticated user

Add WHERE clause to filter feedback records by the authenticated user's
ID, preventing any authenticated user from reading all users' copilot
interactions, queries, and workflow YAML (IDOR / CWE-639).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(smtp): add SSRF validation and genericize network error messages

Prevent SSRF via user-controlled smtpHost by validating with
validateDatabaseHost before creating the nodemailer transporter.
Collapse distinct network error messages (ECONNREFUSED, ECONNRESET,
ETIMEDOUT) into a single generic message to prevent port-state leakage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): add SSRF validation to SFTP/SSH and access control to workspace invitations

Add `validateDatabaseHost` checks to SFTP and SSH connection utilities to
block connections to private/reserved IPs and localhost, matching the
existing pattern used by all database tools. Add authorization check to
the workspace invitation GET endpoint so only the invitee or a workspace
admin can view invitation details.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(smtp): restore SMTP response code handling for post-connection errors

SMTP 4xx/5xx response codes are application-level errors (invalid
recipient, mailbox full, server error) unrelated to the SSRF hardening
goal. Restore response code differentiation and logging to preserve
actionable user-facing error messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): use session email directly instead of extra DB query

Addresses PR review feedback — align with the workspace invitation
route pattern by using session.user.email instead of re-fetching
from the database.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* lint

* fix(auth): revert lint autofix that broke hasExternalApiCredentials return type

Biome auto-fixed `return auth !== null && auth.startsWith(...)` to
`return auth?.startsWith(...)` which returns `boolean | undefined`,
not `boolean`, causing a TypeScript build failure.

* fix(smtp): pin resolved IP to prevent DNS rebinding (TOCTOU)

Use the pre-resolved IP from validateDatabaseHost instead of the
original hostname when creating the nodemailer transporter. Set
servername to the original hostname to preserve TLS SNI validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(security): extract createPinnedLookup helper for DNS rebinding prevention

Extract reusable createPinnedLookup from secureFetchWithPinnedIP so
non-HTTP transports (SSH, SFTP, IMAP) can pin resolved IPs at the
socket level. SMTP route uses host+servername pinning instead since
nodemailer doesn't reliably pass lookup to both secure/plaintext paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): pin IMAP connections to validated resolved IP

Pass the resolved IP from validateDatabaseHost to ImapFlow as host,
with the original hostname as servername for TLS SNI verification.
Closes the DNS TOCTOU rebinding window.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* lint

* fix(auth): revert lint autofix on hasExternalApiCredentials return type

Also pin SFTP/SSH connections to validated resolved IP to prevent DNS rebinding.

* fix(security): short-circuit admin check when caller is invitee

Skip the hasWorkspaceAdminAccess DB query when the caller is already
the invitee, avoiding an unnecessary round-trip. Aligns with the org
invitation route pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 18:10:47 -07:00
Waleed
a7c1e510e6 fix(knowledge): reject non-alphanumeric file extensions from document names (#3816)
* fix(knowledge): reject non-alphanumeric file extensions from document names

* fix(knowledge): improve error message when extension is non-alphanumeric
2026-03-27 16:58:07 -07:00
Vikhyath Mondreti
271624a402 fix(linear): add default null for after cursor (#3814) 2026-03-27 15:57:40 -07:00
Vikhyath Mondreti
dda012eae9 feat(concurrency): bullmq based concurrency control system (#3605)
* feat(concurrency): bullmq based queueing system

* fix bun lock

* remove manual execs off queues

* address comments

* fix legacy team limits

* cleanup enterprise typing code

* inline child triggers

* fix status check

* address more comments

* optimize reconciler scan

* remove dead code

* add to landing page

* Add load testing framework

* update bullmq

* fix

* fix headless path

---------

Co-authored-by: Theodore Li <teddy@zenobiapay.com>
2026-03-27 13:11:35 -07:00
Vikhyath Mondreti
2dd6d3d1e6 fix(import): dedup workflow name (#3813) 2026-03-27 13:09:49 -07:00
Waleed
b90bb75cda fix(knowledge): connector spinner race condition + connectors column (#3812)
* fix(knowledge): scope sync/update state per-connector to prevent race conditions

* feat(knowledge): add connectors column to knowledge base list

* refactor(knowledge): extract set helpers, handleTogglePause, and filter-before-map

* refactor(knowledge): use onSettled for syncingIds cleanup, consistent with updatingIds
2026-03-27 12:54:14 -07:00
Waleed
fb233d003d fix(flyout): align inline rename with non-rename styling (#3811) 2026-03-27 12:39:23 -07:00
Waleed
34df3333d1 fix(knowledge): fix search input flicker on clear and plan display name fallback (#3810) 2026-03-27 12:23:41 -07:00
Waleed
23677d41a0 improvement(sidebar): collapsed sidebar UX, quick-create, hover consistency, and UI polish (#3807)
* improvement(sidebar): collapsed sidebar UX, quick-create, hover consistency, and UI polish

Made-with: Cursor

* fix(sidebar): use stable handlers for root workflow items instead of inline lambdas

Made-with: Cursor

* fix(sidebar): reset actionsOpen state before triggering rename in collapsed dropdown

Made-with: Cursor
2026-03-27 12:08:17 -07:00
Waleed
a489f91085 fix(knowledge): show spinner on connector chip while syncing (#3808)
* fix(knowledge): show spinner on connector chip while syncing

* fix(knowledge): scope sync spinner to mutation lifetime, not cooldown
2026-03-27 12:04:11 -07:00
Adithya Krishna
ed6e7845cc chore: fix rerenders on files (#3805)
* chore: fix rerenders on files

* chore: fix review changes
2026-03-27 11:48:51 -07:00
Adithya Krishna
e698f9fe14 chore: remove font antialiasing (#3806)
* chore: fix antialiasing

* chore: remove antialiasing
2026-03-27 11:29:37 -07:00
Adithya Krishna
db1798267e feat: update sidebar and knowledge (#3804)
* feat: update sidebar and knowledge

* chore: fix rernders on knowledge

* chore: fix review changes

* chore: fix review changes
2026-03-27 09:39:41 -07:00
Waleed
5f1d5e0618 feat(generic): add generic resource tab, refactor home structure, and UI polish (#3803)
* feat(generic): add generic resource tab, refactor home structure, and UI polish

* reverted hardcoded ff

* fix build

* styling consistency

* styling

* fix(auth): extract shared auth button class and align SSO primary style

- Extract AUTH_SUBMIT_BTN constant to (auth)/components/auth-button-classes.ts,
  replacing 10 copy-pasted identical className strings across 7 files
- Update SSOLoginButton primary variant to use AUTH_SUBMIT_BTN instead of
  hardcoded purple gradient, making it consistent with all other auth form
  submit buttons
- Fix missing isEphemeralResource import in lib/copilot/resources.ts
  (was re-exported but not available in local scope)

* fix(auth): replace inline button class in chat auth components with AUTH_SUBMIT_BTN

* fix send button hover state
2026-03-27 00:13:41 -07:00
Vikhyath Mondreti
ed5645166e improvement(terminal): performance improvements (#3796)
* improvement(terminal): prevent canvas crashes

* checkpoint

* make persistence execution scoped not debounce

* revert feature flags

* address bugbot comments

* fix test

* fix

* fix type

* fix abortion of manual run

* fix type errors

* fix diff comment

* fix chat history query

* fix comment

* Ignore previously executed tool call ids

* fix eval input color

* fix copilot run workflow

* fix tests

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-03-26 22:52:23 -04:00
Theodore Li
50e42c2041 fix(ui): Change modal field to be company size (#3801)
* Change modal field to be company size

* Adjust dropdown options

* Fix lint

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-03-26 22:47:59 -04:00
Waleed
e70e1ec8c5 feat(search): add tables, files, knowledge bases, and jobs to cmd-k search (#3800)
* feat(search): add tables, files, knowledge bases, and jobs to cmd-k search

* fix(search): address PR feedback — drop files/jobs, add onSelect to memo

* fix(search): add files back with per-file deep links, keep jobs out

* fix(search): remove onSelect from memo comparator to match existing pattern
2026-03-26 19:18:21 -07:00
Waleed
4c474e03c1 fix(connectors): contentDeferred pattern + validation fixes across all connectors (#3793)
* fix(knowledge): enqueue connector docs per-batch to survive sync timeouts

* fix(connectors): convert all connectors to contentDeferred pattern and fix validation issues

All 10 connectors now use contentDeferred: true in listDocuments, returning
lightweight metadata stubs instead of downloading content during listing.
Content is fetched lazily via getDocument only for new/changed documents,
preventing Trigger.dev task timeouts on large syncs.

Connector-specific fixes from validation audit:
- Google Drive: metadata-based contentHash, orderBy for deterministic pagination,
  precise maxFiles, byte-length size check with truncation warning
- OneDrive: metadata-based contentHash, orderBy for deterministic pagination
- SharePoint: metadata-based contentHash, byte-length size check
- Dropbox: metadata-based contentHash using content_hash field
- Notion: code/equation block extraction, empty page fallback to title,
  reduced CHILD_PAGE_CONCURRENCY to 5, syncContext parameter
- Confluence: syncContext caching for cloudId, reduced label concurrency to 5
- Gmail: use joinTagArray for label tags
- Obsidian: syncRunId-based stub hash for forced re-fetch, mtime-based hash
  in getDocument, .trim() on vaultUrl, lightweight validateConfig
- Evernote: retryOptions threaded through apiFindNotesMetadata and apiGetNote
- GitHub: added contentDeferred: false to getDocument, syncContext parameter

Infrastructure:
- sync-engine: added syncRunId to syncContext for Obsidian change detection
- confluence/utils: replaced raw fetch with fetchWithRetry, added retryOptions
- oauth: added supportsRefreshTokenRotation: false for Dropbox
- Updated add-connector and validate-connector skills with contentDeferred docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(connectors): address PR review comments - metadata merge, retryOptions, UTF-8 safety

- Sync engine: merge metadata from getDocument during deferred hydration,
  so Gmail/Obsidian/Confluence tags and metadata survive the stub→full transition
- Evernote: pass retryOptions {retries:3, backoff:500} from listDocuments and
  getDocument callers into apiFindNotesMetadata and apiGetNote
- Google Drive + SharePoint: safe UTF-8 truncation that walks back to the last
  complete character boundary instead of splitting multi-byte chars

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(evernote): use correct RetryOptions property names

maxRetries/initialDelayMs instead of retries/backoff to match the
RetryOptions interface from lib/knowledge/documents/utils.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(sync-engine): merge title from getDocument and skip unchanged docs after hydration

- Merge title from getDocument during deferred hydration so Gmail
  documents get the email Subject header instead of the snippet text
- After hydration, compare the hydrated contentHash against the stored
  DB hash — if they match, skip the update. This prevents Obsidian
  (and any connector with a force-refresh stub hash) from re-uploading
  and re-processing unchanged documents every sync

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(sync-engine): dedup externalIds, enable deletion reconciliation, merge sourceUrl

Three sync engine gaps identified during audit:

1. Duplicate externalId guard: if a connector returns the same externalId
   across pages (pagination overlap), skip the second occurrence to prevent
   unique constraint violations on add and double-uploads on update.

2. Deletion reconciliation: previously required explicit fullSync or
   syncMode='full', meaning docs deleted from the source accumulated in
   the KB forever. Now runs on all non-incremental syncs (which return
   ALL docs). Includes a safety threshold: if >50% of existing docs
   (and >5 docs) would be deleted, skip and warn — protects against
   partial listing failures. Explicit fullSync bypasses the threshold.

3. sourceUrl merge: hydration now picks up sourceUrl from getDocument,
   falling back to the stub's sourceUrl if getDocument doesn't set one.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* lint

* fix(connectors): confluence version metadata fallback and google drive maxFiles guard

- Confluence: use `version?.number` directly (undefined) in metadata instead
  of `?? ''` (empty string) to prevent Number('') = 0 passing NaN check in
  mapTags. Hash still uses `?? ''` for string interpolation.
- Google Drive: add early return when previouslyFetched >= maxFiles to prevent
  effectivePageSize <= 0 which violates the API's pageSize requirement (1-1000).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(connectors): blogpost labels and capped listing deletion reconciliation

- Confluence: fetchLabelsForPages now tries both /pages/{id}/labels and
  /blogposts/{id}/labels, preventing label loss when getDocument hydrates
  blogpost content (previously returned empty labels on 404).
- Sync engine: skip deletion reconciliation when listing was capped
  (maxFiles/maxThreads). Connectors signal this via syncContext.listingCapped.
  Prevents incorrect deletion of docs beyond the cap that still exist in source.
  fullSync override still forces deletion for explicit cleanup.
- Google Drive & Gmail: set syncContext.listingCapped = true when cap is hit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(connectors): set syncContext.listingCapped in all connectors with caps

OneDrive, Dropbox, SharePoint, Confluence (v2 + CQL), and Notion (3 listing
functions) now set syncContext.listingCapped = true when their respective
maxFiles/maxPages limit is hit. Without this, the sync engine's deletion
reconciliation would run against an incomplete listing and incorrectly
hard-delete documents that exist in the source but fell outside the cap window.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(evernote): thread retryOptions through apiListTags and apiListNotebooks

All calls to apiListTags and apiListNotebooks in both listDocuments and
getDocument now pass retryOptions for consistent retry protection across
all Thrift RPC calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 19:04:54 -07:00
Vikhyath Mondreti
b0980b1e09 fix(light): tag dropdown, code highlight (#3799)
* fix(light): tag dropdown, code highlight

* remove more hardcoded vals
2026-03-26 18:15:52 -07:00
Waleed
66ce673629 fix(security): harden auth, SSRF, injection, and CORS across API routes (#3792)
* fix: prevent auth bypass via user-controlled context query param in file serve

The /api/files/serve endpoint trusted a user-supplied `context` query
parameter to skip authentication. An attacker could append
`?context=profile-pictures` to any file URL and download files without
auth. Now the public access gate checks the key prefix instead of the
query param, and `og-images/` is added to `inferContextFromKey`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use randomized heredoc delimiter in SSH execute-script route

Prevents accidental heredoc termination if script content contains
the delimiter string on its own line.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: escape workingDirectory in SSH execute-command route

Use escapeShellArg() with single quotes for the workingDirectory
parameter, consistent with all other SSH routes (execute-script,
create-directory, delete-file, move-rename).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: harden chat/form deployment auth (OTP brute-force, CSPRNG, HMAC tokens)

- Add brute-force protection to OTP verification with attempt tracking (CWE-307)
- Replace Math.random() with crypto.randomInt() for OTP generation (CWE-338)
- Replace unsigned Base64 auth tokens with HMAC-SHA256 signed tokens (CWE-327)
- Use shared isEmailAllowed utility in OTP route instead of inline duplicate
- Simplify Redis OTP update to single KEEPTTL call

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: harden SSRF protections and input validation across API routes

Add DNS-based SSRF validation for MCP server URLs, secure OIDC discovery
with IP-pinned fetch, strengthen OTP/chat/form input validation, sanitize
1Password vault parameters, and tighten deployment security checks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* lint

* fix(file-serve): remove user-controlled context param from authenticated path

The `?context` query param was still being passed to `handleCloudProxy`
in the authenticated code path, allowing any logged-in user to spoof
context as `profile-pictures` and bypass ownership checks in
`verifyFileAccess`. Now always use `inferContextFromKey` from the
server-controlled key prefix.

* fix: handle legacy OTP format in decodeOTPValue for deploy-time compat

Add guard for OTP values without colon separator (pre-deploy format)
to avoid misparse that would lock out users with in-flight OTPs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(mcp): distinguish DNS resolution failures from SSRF policy blocks

DNS lookup failures now throw McpDnsResolutionError (502) instead of
McpSsrfError (403), so transient DNS hiccups surface as retryable
upstream errors rather than confusing permission rejections.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: make OTP attempt counting atomic to prevent TOCTOU race

Redis path: use Lua script for atomic read-increment-conditional-delete.
DB path: use optimistic locking (UPDATE WHERE value = currentValue) with
re-read fallback on conflict. Prevents concurrent wrong guesses from
each counting as a single attempt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: check attempt count before OTP comparison to prevent bypass

Reject OTPs that have already reached max failed attempts before
comparing the code, closing a race window where a correct guess
could bypass brute-force protection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: validate OIDC discovered endpoints against SSRF

The discovery URL itself was SSRF-validated, but endpoint URLs returned
in the discovery document (tokenEndpoint, userInfoEndpoint, jwksEndpoint)
were stored without validation. A malicious OIDC issuer on a public IP
could return internal network URLs in the discovery response.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove duplicate OIDC endpoint SSRF validation block

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: validate OIDC discovered endpoints and pin DNS for 1Password Connect

- SSRF-validate all endpoint URLs returned by OIDC discovery documents
  before storing them (authorization, token, userinfo, jwks endpoints)
- Pin DNS resolution in 1Password Connect requests using
  secureFetchWithPinnedIP to prevent TOCTOU DNS rebinding attacks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* lint

* fix: replace KEEPTTL with TTL+EX for Redis <6.0 compat, add DB retry loop

- Lua script now reads TTL and uses SET...EX instead of KEEPTTL
- DB optimistic locking now retries up to 3 times on conflict

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address review feedback on OTP atomicity and 1Password fetch

- Replace Redis KEEPTTL with TTL+SET EX for Redis <6.0 compatibility
- Add retry loop to DB optimistic lock path so concurrent OTP attempts
  are actually counted instead of silently dropped
- Remove unreachable fallback fetch in 1Password Connect; make
  validateConnectServerUrl return non-nullable string

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: treat Lua nil return as locked when OTP key is missing

When the Redis key is deleted/expired between getOTP and
incrementOTPAttempts, the Lua script returns nil. Handle this
as 'locked' instead of silently treating it as 'incremented'.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: handle Lua nil as locked OTP and add SSRF check to MCP env resolution

- Treat Redis Lua nil return (expired/deleted key) as 'locked' instead
  of silently treating it as a successful increment
- Add validateMcpServerSsrf to MCP service resolveConfigEnvVars so
  env-var URLs are SSRF-validated after resolution at execution time

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: narrow resolvedIP type guard instead of non-null assertion

Replace urlValidation.resolvedIP! with proper type narrowing by adding
!urlValidation.resolvedIP to the guard clause, so TypeScript can infer
the string type without a fragile assertion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: bind auth tokens to deployment password for immediate revocation

Include a SHA-256 hash of the encrypted password in the HMAC-signed
token payload. Changing the deployment password now immediately
invalidates all existing auth cookies, restoring the pre-HMAC behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: bind auth tokens to deployment password and remove resolvedIP non-null assertion

- Include SHA-256 hash of encryptedPassword in HMAC token payload so
  changing a deployment's password immediately invalidates all sessions
- Pass encryptedPassword through setChatAuthCookie/setFormAuthCookie
  and validateAuthToken at all call sites
- Replace non-null assertion on resolvedIP with proper narrowing guard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: update test assertions for new encryptedPassword parameter

Tests now expect the encryptedPassword arg passed to validateAuthToken
and setDeploymentAuthCookie after the password-binding change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: format long lines in chat/form test assertions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: pass encryptedPassword through OTP route cookie generation

Select chat.password in PUT handler DB query and pass it to
setChatAuthCookie so OTP-issued tokens include the correct
password slot for subsequent validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 16:32:23 -07:00
Waleed
f37e4b67c7 feat(ketch): add Ketch privacy consent integration (#3794)
* feat(ketch): add Ketch privacy consent integration

* fix(ketch): add response.ok guards and fix registry ordering

* fix(ketch): include errorMessage in error response output for all tools

* fix(ketch): wire optional purposes filter for get_consent operation
2026-03-26 15:49:58 -07:00
Waleed
7a1a46067d feat(granola): add Granola meeting notes integration (#3790)
* feat(granola): add Granola meeting notes integration

* fix(granola): use string comparison for includeTranscript to avoid truthy string bug

* fix(granola): add missing get_note output fields to block definition

* regen docs
2026-03-26 15:00:00 -07:00
Adithya Krishna
bf60670c0b chore: fix cn with tw-merge (#3789)
* chore: fix cn

* chore: fix scale
2026-03-26 13:30:21 -07:00
Waleed
8a481b612d chore(config): clean up bun, turbo, and next.js config (#3788)
* chore(config): clean up bun, turbo, and next.js config

* chore(ci): bump bun to 1.3.11 in dockerfiles and workflows
2026-03-26 13:04:23 -07:00
Adithya Krishna
bc4b7f5759 feat: fix rerenders on search input (#3784)
* chore: fix conflicts

* chore: update contents

* chore: fix review changes
2026-03-26 12:06:31 -07:00
Adithya Krishna
5aa0b4d5d4 fix: emcn component library design engineering polish (#3672)
* chore: fix conflicts

* chore: fix conflicts

* chore: pause marquee

* chore: fix conflicts

* chore: fix conflicts

* chore: address review comments

* chore: fix conflicts

* chore: other bug fixes

* chore: fix conflicts

* chore: fix radius

* chore: fix review changes

* chore: fix conflicts

* chore: revert other

* chore: fix conflicts

* chore: fix review changes

* chore: fix conflicts

* chore: fix review changes

* chore: fix button state

* chore: fix button state

* chore: fix review changes

* chore: fix lint

* chore: fix conflicts

* chore: add metadata

* chore: fix things

* chore: fix overwritten states

* chore: fix warnings

* chore: fix button state

* chore: fix review changes

* chore: fix review changes

* chore: fix hover state

* chore: fix popover

* chore: fix review changes

* chore: fix review changes
2026-03-26 11:40:23 -07:00
Waleed
3774f33d39 fix(landing): fix image rendering and navbar blog/docs navigation (#3785)
* fix(landing): fix image rendering and navbar blog/docs navigation

* fix(navbar): remove disclosure aria attributes from navigation links
2026-03-26 09:48:54 -07:00
Waleed
3597eacdb7 feat(demo-request): block personal email domains (#3786) 2026-03-26 09:47:03 -07:00
Waleed
c5fe92567a fix(blog): restore unoptimized prop on blog cover images (#3782) 2026-03-26 01:13:40 -07:00
Waleed
36bc57f0b9 fix(ui): polish subscription billing settings (#3781)
* fix(ui): polish subscription billing settings

Made-with: Cursor

* fix(ui): trigger purchase refresh on success

Made-with: Cursor
2026-03-25 23:44:05 -07:00
Waleed
2771b679cb fix(copilot): expand tool metadata, fix thinking text rendering, clean up display logic (#3779)
* fix(copilot): expand tool metadata, fix thinking text rendering, clean up display logic

* fix(copilot): guard null reasoning data, use ensureTextBlock for thinking end

* fix(copilot): restore displayTitle precedence so cancelled tools show 'Stopped by user'
2026-03-25 22:53:58 -07:00
Vikhyath Mondreti
6610c37e10 Merge branch 'staging' of github.com:simstudioai/sim into staging 2026-03-25 21:01:00 -07:00
Vikhyath Mondreti
a9fc1a24a9 fix(guard-change): run finalize at right time 2026-03-25 21:00:44 -07:00
Waleed
d97e22e395 chore(docs): update readme (#3778) 2026-03-25 20:53:27 -07:00
Vikhyath Mondreti
9603fd091b Merge branch 'staging' of github.com:simstudioai/sim into staging 2026-03-25 20:32:49 -07:00
Vikhyath Mondreti
9e4fc5024f fix(retry): extract code into callback 2026-03-25 20:32:39 -07:00
Vikhyath Mondreti
1a14f4c13d fix chatHistory reconnect effect 2026-03-25 20:28:15 -07:00
Waleed
7583c8fbf4 feat(misc): skills import, MCP modal, workmark, dispatch modals, collapsed tasks and workflows manipulation, README (#3777)
* feat: skills import, MCP modal updates, wordmark icon, tool-input improvements

- Add skills import functionality (route + components + utils)
- Update MCP deploy modal
- Add Wordmark emcn icon + logo SVG assets
- Improve tool-input component
- Update README branding to new wordmark
- Add ban-spam-accounts admin script

* fix: resolve build error and audit findings from simplify review

- Add BUILT_IN_TOOL_TYPES export to blocks/utils.ts (was removed from
  tool-input.tsx but never added to the new import target — caused build
  error "Export BUILT_IN_TOOL_TYPES doesn't exist in target module")
- Export Wordmark from emcn icons barrel (index.ts)
- Derive isDragging from dragCounter in skill-import.tsx instead of
  maintaining redundant state that could desync
- Replace manual AbortController/setTimeout with AbortSignal.timeout()
  in skills import API route (Node 17.3+ supported, cleaner no-cleanup)
- Use useId() for SVG gradient ID in wordmark.tsx to prevent duplicate
  ID collisions if rendered multiple times on the same page

* fix(scripts): fix docs mismatch and N+1 query in ban-spam-accounts

- Fix comment: default pattern is @vapu.xyz, not @sharebot.net
- Replace per-user stats loop with a single aggregated JOIN query

* feat: wire wordmark into sidebar, fix credential selector modal dispatch

- Show Wordmark (icon + text) in the expanded sidebar instead of the
  bare Sim icon; collapsed state keeps the small Sim icon unchanged
- Untrack scripts/ban-spam-accounts.ts (gitignored; one-off script)
- Credential selector: open OAuthRequiredModal inline instead of
  navigating to Settings → Integrations (matches MCP/tool-input pattern)
- Credential selector: update billing import from getSubscriptionAccessState
  to getSubscriptionStatus; drop writePendingCredentialCreateRequest and
  useSettingsNavigation dependencies

* feat(misc): misc UX/UI improvements

* more random fixes

* more random fixes

* fix: address PR review findings from cursor bugbot

- settings-sidebar: use getSubscriptionAccessState instead of getSubscriptionStatus
  so billingBlocked and status validity are checked; add requiresMax gating so
  max-plan-only nav items (inbox) are hidden for lower-tier users
- credential-selector: same getSubscriptionAccessState migration for credential sets
  visibility check
- mothership chats PATCH: change else if to if for isUnread so both title and
  isUnread can be updated in a single request
- skills import: check Content-Length header before reading response body to avoid
  loading oversized files into memory

* fix(skills): add ZIP file size guard before extraction

Checks file.size > 5 MB before calling extractSkillFromZip to prevent
zip bombs from exhausting browser memory at the client-side upload path.

* feat(settings-sidebar): show locked upsell items with plan badge

Sim Mailer (requiresMax) and Email Polling (requiresTeam) now always
appear in the settings sidebar when billing is enabled and the
deployment is hosted. If the user lacks the required plan they see a
small MAX / TEAM badge next to the label and are taken to the page
which already contains the upgrade prompt.

Enterprise (Access Control, SSO) and Team management stay hard-hidden
for lower tiers. Admin/superuser items stay truly hidden.

* fix(settings-sidebar): remove flex-1 from label span to fix text centering

* feat(settings-sidebar): remove team gate from email polling, keep only mailer max gate

* feat(subscription): billing details layout and Enterprise card improvements

- Move Enterprise plan card into the plan grid (auto-fit columns) instead
  of a separate standalone section below billing details
- Refactor billing details section: remove outer border/background,
  separate each row with top border + padding for cleaner separation
- Update button variants: Add Credits → active, Invoices → active

* fix(mothership): prevent lastSeenAt conflict when both title and isUnread are patched together

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(sidebar): prevent double-save race in flyout inline rename on Enter+blur

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(skills): normalize CRLF line endings before parsing SKILL.md frontmatter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 20:06:15 -07:00
Vikhyath Mondreti
7b96b0e8e8 add logs 2026-03-25 20:05:23 -07:00
Vikhyath Mondreti
794d5eab5e fix(explicit-user-abort): separate explicit user abort semantics (#3776)
* fix(explicit-user-abort): separate explicit user abort semantics

* address comments

* more
2026-03-25 19:39:14 -07:00
Vikhyath Mondreti
5a5c33d326 fix(client): network drops reconnecting behaviour (#3775)
* fix(client): network drops reconnecting behaviour

* address bugbot comments

* address comments

* address queued message conflicts during retries

* fix more review comments

* fix branch

* fix non-clear bug

* fix
2026-03-25 17:34:23 -07:00
Waleed
104ad03004 fix(notifications): auto-dismiss info-level workflow notifications (#3774) 2026-03-25 16:04:13 -07:00
Theodore Li
9d1b9763c5 Feat(logs) upgrade mothership chat messages to error (#3772)
* feat(log): enable info logs in staging and prod

* Upgrade info logs to error for message route

* Add to orchestrator, remove helm shennanigans

* Fix lint

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-03-25 18:45:17 -04:00
Theodore Li
be6b00d95f feat(ui): add request a demo modal (#3766)
* fix(ui): add request a demo modal

* Remove dead code

* Remove footer modal

* Address greptile comments

* Sanatize CRLF characters from emails

* extract shared email header safety regex

Co-authored-by: Theodore Li <TheodoreSpeaks@users.noreply.github.com>

* Use pricing CTA action for demo modal

Co-authored-by: Theodore Li <TheodoreSpeaks@users.noreply.github.com>

* fix demo request import ordering

Co-authored-by: Theodore Li <TheodoreSpeaks@users.noreply.github.com>

* merge staging and fix hubspot list formatting

Co-authored-by: Theodore Li <TheodoreSpeaks@users.noreply.github.com>

* fix(generate-docs): fix tool description extraction and simplify script

- Fix endsWith over-matching: basename === 'index.ts'/'types.ts' instead
  of endsWith(), which was silently skipping valid tool files like
  list_leave_types.ts, delete_index.ts, etc.
- Add extractSwitchCaseToolMapping() to resolve op ID → tool ID mismatches
  where block switch statements map differently (e.g. HubSpot get_carts →
  hubspot_list_carts)
- Fix double fs.readFileSync in writeIntegrationsJson — reuse existing
  fileContent variable instead of re-reading the file
- Remove 5 dead functions superseded by *FromContent variants
- Simplify extractToolsAccessFromContent to use matchAll
- fix(upstash): replace template literal tool ID with explicit switch cases

* fix(generate-docs): restore extractIconName by aliasing to extractIconNameFromContent

* restore

* fix(demo-modal): reset form on open to prevent stale success state on reopen

* undo hardcoded ff

* fix(upstash): throw on unknown operation instead of silently falling back to get

---------

Co-authored-by: Theodore Li <teddy@zenobiapay.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Theodore Li <TheodoreSpeaks@users.noreply.github.com>
Co-authored-by: waleed <walif6@gmail.com>
2026-03-25 15:30:36 -07:00
Vikhyath Mondreti
438defceb0 fix(mothership): key resumes by orchestration id (#3771) 2026-03-25 14:48:28 -07:00
Theodore Li
87e8d3caf8 feat(logs) Add messageId and requestId context to all mothership log messages (#3770)
Co-authored-by: Theodore Li <theo@sim.ai>
2026-03-25 17:27:02 -04:00
Waleed
f94be08950 fix(billing): atomize usage_log and userStats writes via central recordUsage (#3767)
* fix(billing): atomize usage_log and userStats writes via central recordUsage()

* fix(billing): address PR review — re-throw errors, guard reserved keys, handle zero-cost counters

* chore(lint): fix formatting in hubspot list_lists.ts from staging

* fix(billing): tighten early-return guard to handle empty additionalStats object

* lint

* chore(billing): remove implementation-decision comments
2026-03-25 13:41:27 -07:00
Adithya Krishna
54a862d5b0 fix(user-input): fix multiple re-renders on user-input and split the file (#3768)
* feat: fix rerenders

* chore: split user-input
2026-03-25 13:30:50 -07:00
Waleed
e0f2b8fe58 feat(hubspot): add 27 CRM tools and fix OAuth scope mismatch (#3765)
* feat(hubspot): add 27 CRM tools and fix OAuth scope mismatch

* lint

* fix(hubspot): switch marketing events to CRM Objects API and add HubSpotCrmObject base type

* chore(docs): fix import ordering and formatting lint errors

* feat(hubspot): wire all 27 new tools into block definition

* fix(hubspot): address review comments - schema mismatch, pagination, trim, descriptions

- Switch marketing event outputs to CRM envelope structure (id, properties, createdAt, updatedAt, archived) matching CRM Objects API
- Fix list_lists pagination: add offset param, map offset-based response to paging structure
- Add .trim() to contactId/companyId in pre-existing get/update tools
- Fix default limit descriptions (100 → 10) in list_contacts/list_companies
- Fix operator examples (CONTAINS → CONTAINS_TOKEN) in search_contacts/search_companies
- Remove unused params arg in get_users transformResponse

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(hubspot): revert to Marketing Events API and fix Lists pagination per API docs

Marketing Events:
- Revert from /crm/v3/objects/marketing_events back to /marketing/v3/marketing-events
- The Marketing Events API does NOT require appId for GET /marketing-events/{objectId}
- appId is only needed for the /events/{externalEventId} endpoint (which we don't use)
- Restore flat response schema (objectId, eventName, etc. at top level, not CRM envelope)

Lists:
- POST /crm/v3/lists/search uses offset-based pagination (not cursor-based)
- Response shape: { lists, hasMore, offset, total } — not { results, paging }
- Map offset → paging.next.after for consistent block interface
- Fix default count: 20 (not 25), max 500
- GET /crm/v3/lists/{listId} wraps response in { list: { ... } }

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(hubspot): final audit fixes verified against API docs

- Revert list_contacts/list_companies default limit back to 100 (confirmed by API docs)
- Add idProperty param to get_appointment.ts (was missing, inconsistent with update_appointment)
- Remove get_carts from idProperty block condition (carts don't support idProperty)
- Add get_lists to after block condition (pagination was inaccessible from UI)
- Add after pagination param to get_users.ts (was missing, users beyond first page unreachable)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(hubspot): return paging in get_users and add to block after condition

- Add paging output to get_users transformResponse and outputs
- Add get_users to block after subBlock condition so cursor is accessible from UI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(hubspot): align total fallback with type definitions in search tools

Use `?? 0` instead of `?? null` for search tools where the type declares
`total: number`. Also declare `total` in list_lists metadata output schema.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 13:15:48 -07:00
Waleed
2691c12747 feat(rippling): add Rippling HR integration with 19 tools (#3764)
* feat(rippling): add Rippling HR integration with 19 tools

* fix(rippling): address PR review feedback

- Fix lint:check import ordering in icon-mapping.ts
- Build clean params object instead of spreading all UI fields to API
- Add try/catch around JSON.parse for users field
- Use != null guard for limit/offset to not drop 0 values
- Add missing tags to block config and integrations.json

* fix(rippling): guard startDate by operation and clarify totalCount descriptions

- Guard startDate/endDate with operation check to prevent candidateStartDate
  from clobbering date filters on leave/activity operations
- Update totalCount output descriptions on paginated tools to clarify it
  reflects page size, not total record count

* fix(rippling): use null-safe guard for groupVersion param

* fix(rippling): remove operation field from tool params payload

* fix(rippling): add input validation for action param and empty group update body
2026-03-25 12:02:19 -07:00
Theodore Li
8caaf01371 fix(ui): fix kb id extraction logic for resource, sync tags (#3763)
* fix(ui): fix kb id extraction logic for resource, sync tags

* Pass knowledge base id back on edit tag

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-03-25 13:46:57 -04:00
Daniel Gómez Martínez
668b948f0b feat(agents): generalize repository guidance for coding agents (#3760)
* feat(agents): generalize repository guidance for coding agents

* fix(agents): use repo-root link in sim app guidance
2026-03-25 10:29:47 -07:00
Vikhyath Mondreti
8800f03fa3 improvement(billing): treat past_due state correctly (#3750)
* improvement(billing): treat past_due state correctly

* toggle fixes

* address comments

* Fix type errors
2026-03-24 22:28:39 -07:00
Vikhyath Mondreti
b497033795 Revert "improvement(mothership): show continue options on abort (#3746)" (#3756)
This reverts commit b9926df8e0.
2026-03-24 21:08:52 -07:00
Waleed
666dc67aa2 fix(db): use bigint for token counter columns in user_stats (#3755) 2026-03-24 21:08:07 -07:00
Waleed
7af7a225f2 fix(knowledge): route connector doc processing through queue instead of fire-and-forget (#3754)
* fix(knowledge): route connector doc processing through queue instead of fire-and-forget

* fix(knowledge): rename jobIds to batchIds in processDocumentsWithTrigger return type

* improvement(knowledge): add Trigger.dev tags for connector sync and document processing tasks

* fix(knowledge): move completeSyncLog after doc enqueue, handle NULL processingStartedAt in stuck doc query
2026-03-24 21:07:55 -07:00
Waleed
228578e282 fix(auth): remove captcha from login, fix signup captcha flow (#3753)
* fix(auth): remove captcha from login, fix signup captcha flow

* fix(auth): show Turnstile widget at normal size for Managed mode challenges
2026-03-24 20:36:49 -07:00
Waleed
be647469ac fix(ui): constrain tooltip width and remove question mark cursor (#3752)
- Add max-w-[260px] to Tooltip.Content so video previews don't blow out the tooltip size
- Replace cursor-help with cursor-default on info icons in settings
2026-03-24 19:01:45 -07:00
Waleed
96b171cf74 improvement(tour): fix tour auto-start logic and standardize selectors (#3751)
* improvement(tour): fix tour auto-start logic and standardize selectors

* fix(tour): address PR review comments

- Move autoStartAttempted.add() inside timer callback to prevent
  blocking auto-start when tour first mounts while disabled
- Memoize setJoyrideRef with useCallback to prevent ref churn
- Remove unused joyrideRef
2026-03-24 18:32:17 -07:00
Theodore Li
cdea2404e3 improvement(ui): Merge ui components for mothership chat (#3748)
* improvement(ui): Merge ui definitions for mothership chat

* Fix lint

* Restore copilot layout

* Fix subagent text not animating collapses

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-03-24 20:19:04 -04:00
Waleed
f6975fc0a3 feat(settings): add video tooltip previews for canvas settings (#3749)
* feat(settings): add video tooltip previews for canvas settings

* fix(tooltip): add preload=none and handle query strings in video detection
2026-03-24 16:56:10 -07:00
Theodore Li
59182d5db2 feat(admin): Add assume user capability (#3742)
* Allow admin users to assume user sessions

* Add explicit role check

* Fix lint

* Remove admin panel when impersonating

* Fix lint

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-03-24 19:38:06 -04:00
Siddharth Ganesan
b9926df8e0 improvement(mothership): show continue options on abort (#3746)
* Show continue options on abort

* Fix lint

* Fix
2026-03-24 16:08:28 -07:00
Waleed
77eafabb63 feat(billing): add appliesTo plan restriction for coupon codes (#3744)
* feat(billing): add appliesTo plan restriction for coupon codes

* fix(billing): fail coupon creation on partial product resolution
2026-03-24 13:04:55 -07:00
Waleed
34ea99e99d feat(home): auth-aware landing page navigation (#3743)
* feat(home): auth-aware landing page navigation

- Redirect authenticated users from / to /workspace via middleware (?home param bypasses)
- Show "Go to App" instead of "Log in / Get started" in navbar for authenticated users
- Logo links to /?home for authenticated users to stay in marketing context
- Settings "Home Page" button opens /?home
- Handle isPending session state to prevent CTA button flash

* lint

* fix(home): remove stale ?from=nav params in landing nav

* fix(home): preserve ?home param in nav links during session pending state

* lint
2026-03-24 12:59:29 -07:00
Waleed
a7f344bca1 feat(tour): added product tour (#3703)
* feat: add product tour

* chore: updated modals

* chore: fix the tour

* chore: Tour Updates

* chore: fix review changes

* chore: fix review changes

* chore: fix review changes

* chore: fix review changes

* chore: fix review changes

* minor improvements

* chore(tour): address PR review comments

- Extract shared TourState, TourStateContext, mapPlacement, and TourTooltipAdapter
  into tour-shared.tsx, eliminating ~100 lines of duplication between product-tour.tsx
  and workflow-tour.tsx
- Fix stale closure in handleStartTour — add isOnWorkflowPage to useCallback deps
  so Take a tour dispatches the correct event after navigation

* chore(tour): address remaining PR review comments

- Remove unused logger import and instance in product-tour.tsx
- Remove unused tour-tooltip-fade animation from tailwind config
- Remove unnecessary overflow-hidden wrapper around WorkflowTour
- Add border stroke to arrow SVG in tour-tooltip for visual consistency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(tour): address second round of PR review comments

- Remove unnecessary 'use client' from workflow layout (children are already client components)
- Fix ref guard timing issue in TourTooltipAdapter that could prevent Joyride from tracking tooltip on subsequent steps

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(tour): extract shared Joyride config, fix popover arrow overflow

- Extract duplicated Joyride floaterProps/styles into getSharedJoyrideProps()
  in tour-shared.tsx, parameterized by spotlightBorderRadius
- Fix showArrow disabling content scrolling in PopoverContent by wrapping
  children in a scrollable div when arrow is visible

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* lint

* fix(tour): stop running tour when disabled becomes true

Prevents nav and workflow tours from overlapping. When a user navigates
to a workflow page while the nav tour is running, the disabled flag
now stops the nav tour instead of just suppressing auto-start.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tour): move auto-start flag into timer, fix truncate selector conflict

- Move hasAutoStarted flag inside setTimeout callback so it's only set
  when the timer fires, allowing retry if disabled changes during delay
- Add data-popover-scroll attribute to showArrow scroll wrapper and
  exclude it from the flex-1 truncate selector to prevent overflow
  conflict

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tour): remove duplicate overlay on center-placed tour steps

Joyride's spotlight already renders a full-screen overlay via boxShadow.
The centered TourTooltip was adding its own bg-black/55 overlay on top,
causing double-darkened backgrounds. Removed the redundant overlay div.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: move docs link from settings to help dropdown

The Docs link (https://docs.sim.ai) was buried in settings navigation.
Moved it to the Help dropdown in the sidebar for better discoverability.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Adithya Krishna <aadithya794@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 12:45:16 -07:00
Adithya Krishna
7b6149dc23 chore: optimize imports and useShallow (#3740)
* chore: fix conflicts

* chore: fix review changes

* chore: fix review changes

* chore: fix review changes
2026-03-24 10:51:09 -07:00
Waleed
b09a073c29 feat(table): column drag-and-drop reorder (#3738)
* feat(table): column drag-and-drop reorder

* fix(table): remove duplicate onDragEnd call from handleDrop

* fix(table): persist columnOrder on rename/delete and defer delete to onSuccess

* fix(table): prevent stale refs during column drag operations

Fix two bugs in column drag-and-drop:
1. Stale columnWidths ref during rename - compute updated widths inline
   before passing to updateMetadata
2. Escape-cancelled drag still reorders - update dropTargetColumnNameRef
   directly in handleColumnDragLeave to prevent handleColumnDragEnd from
   reading stale ref value

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(table): insert column at correct side when anchor is unordered

When the anchor column isn't in columnOrder, add it first then insert
the new column relative to it, so 'right' insertions appear after the
anchor as expected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 10:15:48 -07:00
Adithya Krishna
8d93c850ba chore: remove lodash (#3741) 2026-03-24 10:10:20 -07:00
Waleed
83eb3ed211 fix(home): voice input text persistence bugs (#3737)
* fix(home): voice input text persistence bugs

* fix(home): gate setIsListening on startRecognition success

* fix(home): handle startRecognition failure in restartRecognition

* fix(home): reset speech prefix on submit while mic is active
2026-03-24 09:55:13 -07:00
Waleed
a783b9d4ce fix(integrations): remove outdated trigger mode text from FAQ (#3739) 2026-03-24 06:47:50 -07:00
Vikhyath Mondreti
0c80438ede fix(mothership): async resume and tool result ordering (#3735)
* fix(mothership):  async resume and tool result ordering

* ensure tool call terminal state

* address comments
2026-03-24 03:47:47 -07:00
Siddharth Ganesan
41a7d247ea fix(mothership): parallel tool calls 2026-03-24 02:45:31 -07:00
Siddharth Ganesan
092525e8aa fix(mothership): abort streamlining (#3734)
* Fixes

* Address bugbot

* Fixes

* Fix

* Fixes

* Fix lint

* Fixes

* Fixes

* Truncate log
2026-03-24 02:17:58 -07:00
Vikhyath Mondreti
8eb45e3057 fix(ppt): dep injection (#3732) 2026-03-23 21:20:43 -07:00
Siddharth Ganesan
852dc93d39 fix(mothership): tool durability (#3731)
* Durability

* Go check

* Fix

* add pptxgen setup to dockerfile

* Update tools

* Fix

* Fix aborts and gen viz

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
2026-03-23 20:39:29 -07:00
Siddharth Ganesan
5e53757ca9 fix(quiver): build fail (#3730)
* Fix test

* Fix schema

* Fix test

* Fix

* Fix
2026-03-23 18:38:54 -07:00
Siddharth Ganesan
775daed2ea fix(mothership): tool call loop (#3729)
* v0

* Fix ppt load

* Fixes

* Fixes

* Fix lint

* Fix wid

* Download image

* Update tools

* Fix lint

* Fix error msg

* Tool fixes

* Reenable subagent stream

* Subagent stream

* Fix edit workflow hydration

* Throw func execute error on error

* Rewrite

* Remove promptForToolApproval flag, fix workflow terminal logs

* Fixes

* Fix buffer

* Fix

* Fix claimed by

* Cleanup v1

* Tool call loop

* Fixes

* Fixes

* Fix subaget aborts

* Fix diff

* Add delegating state to subagents

* Fix build

* Fix sandbox

* Fix lint

---------

Co-authored-by: Waleed <walif6@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Lakee Sivaraya <71339072+lakeesiv@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Theodore Li <teddy@zenobiapay.com>
2026-03-23 18:11:06 -07:00
Waleed
8f793d9c42 feat(quiver): add QuiverAI integration for SVG generation and vectorization (#3728)
* feat(quiver): add QuiverAI integration for SVG generation and vectorization

* fix(quiver): address review feedback — n>1 data loss, error handling, import consistency

* fix(quiver): add files array to image-to-svg response, remove camelCase param leaks
2026-03-23 18:08:35 -07:00
Waleed
dc6f3db4e5 fix(oauth): decode ID token instead of calling Graph API for Microsoft providers (#3727)
* fix(oauth): decode ID token instead of calling Graph API for Microsoft providers

* fix(oauth): fix type error in getMicrosoftUserInfoFromIdToken parameter

* fix(oauth): address review comments - try-catch JSON.parse, fix email fallback order, guard undefined email

* style(oauth): format email fallback chain to single line
2026-03-23 18:01:13 -07:00
Waleed
88bc16b382 improvement(settings): add View Invoices button to subscription billing details (#3726)
* improvement(settings): add View Invoices button to subscription billing details

* lint

* fix(settings): add user-facing error alert on billing portal failure
2026-03-23 17:04:28 -07:00
Vikhyath Mondreti
767db1ce3a fix(autolayout): edits coalesced for same request diffs (#3724)
* fix(autolayout): edits coalesced for same request diffs

* address comments

* address edge signature gen

* perf improvement
2026-03-23 16:59:07 -07:00
Theodore Li
288aa0851b fix(copilot) Allow loop-in-loop workflow edits (#3723)
* Allow loop-in-loop workflow edits

* Fix lint

* Fix orphaned loop-in-loop if parent id not found

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-03-23 19:16:27 -04:00
Waleed
4c8395928a feat(slack): add conversations.create and conversations.invite tools (#3720)
* feat(slack): add conversations.create and conversations.invite tools

* fix(slack): address PR review comments on conversation tools

* feat(slack): wire create/invite conversation tools into Slack block

* lint

* fix(slack): rename channel output to channelInfo to avoid type collision

The block outputs already declare `channel` as type string (channel ID from
send operation). Rename the object output to `channelInfo` to match the
pattern used by get_channel_info and avoid [object Object] rendering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(slack): update output key in docs to match channelInfo rename

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(docs): fix lint errors in auto-generated docs files

Sort imports in icon-mapping.ts and add trailing newline to meta.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* lint

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 15:32:52 -07:00
Waleed
f02f85fded fix(tables): use overflow-clip on header text to allow horizontal scrolling (#3722) 2026-03-23 14:57:39 -07:00
Waleed
24ed2ab995 improvement(settings): add searchable member selector in integrations and secrets (#3721) 2026-03-23 14:51:46 -07:00
Waleed
daed8dbe2f fix(login): move password reset success message inside the form (#3719)
Was rendered above the form between the header and email field.
Now shows above the submit button alongside other form messages.
2026-03-23 11:42:37 -07:00
Waleed
9302a1b392 fix(auth): use absolute positioning for Turnstile container (#3718)
h-0 w-0 overflow-hidden was clipping the iframe, preventing
Turnstile from executing. absolute takes it out of flow without
clipping, fixing both the layout gap and the captcha failure.
2026-03-23 11:24:50 -07:00
Siddharth Ganesan
8294d8c88a fix(mothership): fix build error (#3717) 2026-03-23 11:20:07 -07:00
Siddharth Ganesan
44ceed4c85 improvement(mothership): add file patch tool (#3712)
* v0

* Fix ppt load

* Fixes

* Fixes

* Fix lint

* Fix wid

* Download image

* Update tools

* Fix lint

* Fix error msg

* Tool fixes

* Reenable subagent stream

* Subagent stream

* Fix edit workflow hydration

* Throw func execute error on error

* Sandbox PPTX generation in subprocess with vm.createContext

AI-generated PptxGenJS code was executed via new Function() in both
the server (full Node.js access) and browser (XSS risk). Replace with
a dedicated Node.js subprocess (pptx-worker.cjs) that runs user code
inside vm.createContext with a null-prototype sandbox — no access to
process, require, Buffer, or any Node.js globals. Process-level
isolation ensures a vm escape cannot reach the main process or DB.

File access is brokered via IPC so the subprocess never touches the
database directly, mirroring the isolated-vm worker pattern. Compilation
happens lazily at serve time (compilePptxIfNeeded) rather than on write,
matching industry practice for source-stored PPTX pipelines.

- Add pptx-worker.cjs: sandboxed subprocess worker
- Add pptx-vm.ts: orchestration, IPC bridge, file brokering
- Add /api/workspaces/[id]/pptx/preview: REST-correct preview endpoint
- Update serve route: compile pptxgenjs source to binary on demand
- Update workspace-file.ts: remove unsafe new Function(), store source only
- Update next.config.ts: include pptxgenjs in outputFileTracingIncludes
- Update trigger.config.ts: add pptx-worker.cjs and pptxgenjs to build

* upgrade deps, file viewer

* Fix auth bypass, SSRF, and wrong size limit comment

- Add 'patch' to workspace_file WRITE_ACTIONS — patch operation was
  missing, letting read-only users modify file content
- Add download_to_workspace_file to WRITE_ACTIONS with '*' wildcard —
  tool was completely ungated, letting read-only users write workspace files
- Update isActionAllowed to handle '*' (always-write tools) and undefined
  action (tools with no operation/action field)
- Block private/internal URLs in download_to_workspace_file to prevent
  SSRF against RFC 1918 ranges, loopback, and cloud metadata endpoints
- Fix file-reader.ts image size limit comment and error message (was 20MB,
  actual constant is 5MB)

* Fix Buffer not assignable to BodyInit in preview route

Wrap Buffer in Uint8Array for NextResponse body — Buffer is not
directly assignable to BodyInit in strict TypeScript mode.

* Fix SSRF bypass, IPv6 coverage, download size cap, and missing deps

- Validate post-redirect URL to block SSRF via open redirectors
- Expand IPv6 private range blocking: fe80::/10, fc00::/7, ::ffff: mapped
- Add 50 MB download cap (Content-Length pre-check + post-buffer check)
- Add refetchOnWindowFocus: 'always' to useWorkspaceFileBinary
- Add workspaceId to PptxPreview useEffect dependency array

* Replace hand-rolled SSRF guard with secureFetchWithValidation

The previous implementation hand-rolled private-IP detection with regex,
missing edge cases (octal IPs, hex IPs, full IPv6 coverage). The codebase
already has secureFetchWithValidation which uses ipaddr.js, handles DNS
rebinding via IP pinning, validates each redirect target, and enforces a
streaming size cap — removing the need for isPrivateUrl, isPrivateIPv4,
the manual pre/post-redirect checks, and the Content-Length + post-buffer
size checks.

* Fix streaming preview cache ordering and patch ambiguity

- PptxPreview: move streaming content check before cache check so live
  AI-generated previews are never blocked by a warm cache from a prior
  file view
- workspace_file patch: reject edits where the search string matches
  more than one location, preventing silent wrong-location patches
- workspace_file patch: remove redundant Record<string, unknown> cast;
  args is already Zod-validated with the correct field types

* Fix subprocess env leak, unbounded preview spawning, and dead code

- pptx-vm: pass minimal env to worker subprocess so it cannot inherit
  DB URLs, API keys, or other secrets from the Next.js process on a
  vm.createContext escape
- PptxPreview: add AbortController so in-flight preview fetch is
  cancelled when the effect re-runs (e.g. next SSE update), preventing
  unbounded concurrent subprocesses; add 500ms debounce on streaming
  renders to reduce subprocess churn during rapid AI generation
- file-reader: remove dead code — the `if (!isReadableType)` guard on
  line 110 was always true (all readable types returned earlier at
  line 76), making the subsequent `return null` unreachable

* Wire abort signal through to subprocess and correct security comment

- generatePptxFromCode now accepts an optional AbortSignal; when the
  signal fires (e.g. client disconnects mid-stream), done() is called
  which clears timers and kills the subprocess immediately rather than
  waiting for the 60s timeout
- preview route passes req.signal so client-side AbortController.abort()
  (from the streaming debounce cleanup) propagates all the way to the
  worker process
- Correct misleading comment in pptx-worker.cjs and pptx-vm.ts:
  vm.createContext is NOT a sandbox when non-primitives are in scope;
  the real security boundary is the subprocess + minimal env

* Remove implementation-specific comments from pptx worker files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix pre-aborted signal, pptx-worker tracing, and binary fetch cache

* Lazy worker path resolution, code size cap, unused param prefix

* Add cache-busting timestamp to binary file fetch

* Fix PPTX cache key stability and attribute-order-independent dimension parsing

* ran lint

---------

Co-authored-by: waleed <walif6@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 11:08:28 -07:00
Adithya Krishna
694b53063b chore: client and server components (#3716) 2026-03-23 10:41:18 -07:00
Waleed
91a0a49264 feat(sidebar): add right-click context menu to settings nav item (#3715)
* feat(sidebar): add right-click context menu to settings nav item

* fix(sidebar): revert settings active highlight

* fix(sidebar): allow modifier-key clicks to open in new tab, make InfisicalIcon black

* update icons
2026-03-23 08:57:36 -07:00
Adithya Krishna
d1310a0c19 chore: optimize all the images (#3713) 2026-03-22 13:38:29 -07:00
Waleed
8e6f1316c4 fix(kb): store filename with .txt extension for connector documents (#3707)
* fix(kb): store filename with .txt extension for connector documents

Connector documents (e.g. Fireflies transcripts) have titles without
file extensions. The DB stored the raw title as filename, but the
processing pipeline extracts file extension from filename to determine
the parser. On retry/reprocess, this caused "Unsupported file type"
errors with the document title treated as the extension.

Now stores processingFilename (which includes .txt) instead of the
raw title, consistent with what was actually uploaded to storage.

* fix(kb): guard stuck document retry against filenames without extension

Existing DB rows may have connector document filenames stored without
a .txt extension (raw meeting titles). The stuck-doc retry path reads
filename from DB and passes it to parseHttpFile, which extracts the
extension via split('.'). When there's no dot, the entire title
becomes the "extension", causing "Unsupported file type" errors.

Falls back to 'document.txt' when the stored filename has no extension.

* fix(kb): fix race condition in stuck document retry during sync

The stuck document retry at the end of each sync was querying for all
documents with processingStatus 'pending' or 'failed'. This included
documents added in the CURRENT sync that were still processing
asynchronously, causing duplicate concurrent processing attempts.

The race between the original (correct) processing and the retry
(which reads the raw title from DB as filename) produced
nondeterministic failures — some documents would succeed while
others would fail with "Unsupported file type: <meeting title>".

Fixes:
- Filter stuck doc query by uploadedAt < syncStartedAt to exclude
  documents from the current sync
- Pass mimeType through to parseHttpFile so text/plain content can
  be decoded directly without requiring a file extension in the
  filename (matches parseDataURI which already handles this)
- Restore filename as extDoc.title in DB (the display name, not
  the processing filename)

* fix(kb): fix race condition in stuck document retry during sync

The stuck document retry at the end of each sync was querying for all
documents with processingStatus 'pending' or 'failed'. This included
documents added in the CURRENT sync that were still processing
asynchronously, causing duplicate concurrent processing attempts.

The race between the original (correct) processing and the retry
(which reads the raw title from DB as filename) produced
nondeterministic failures — some documents would succeed while
others would fail with "Unsupported file type: <meeting title>".

Fixes:
- Filter stuck doc query by uploadedAt < syncStartedAt to exclude
  documents from the current sync
- Pass mimeType through to parseHttpFile and use existing
  getExtensionFromMimeType utility as fallback when filename has
  no extension (e.g. Fireflies meeting titles)
- Apply same mimeType fallback in parseDataURI for consistency

* lint

* fix(kb): handle empty extension edge case in parseDataURI

When filename ends with a dot (e.g. "file."), split('.').pop() returns
an empty string. Fall through to mimeType-based extension lookup
instead of passing empty string to parseBuffer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 03:41:45 -07:00
Siddharth Ganesan
9d6a7f3970 fix(mothership): fix edit hashing (#3711) 2026-03-22 03:06:58 -07:00
Vikhyath Mondreti
4cb5e3469f fix(mothership): minor followups (#3709)
* fix(mothership): abort fix

* diff engine fix
2026-03-22 02:29:09 -07:00
Siddharth Ganesan
59307e22bd fix(mothership): workflow name constraints (#3710)
* Fix

* Fix lint
2026-03-22 02:28:42 -07:00
Vikhyath Mondreti
161424601f fix migration 2026-03-22 01:19:19 -07:00
Siddharth Ganesan
d6bf12da24 improvement(mothership): copilot, files, compaction, tools, persistence, duplication constraints (#3682)
* Improve

* Hide is hosted

* Remove hardcoded

* fix

* Fixes

* v0

* Fix bugs

* Restore settings

* Handle compaction event type

* Add keepalive

* File streaming

* Error tags

* Abort defense

* Edit hashes

* DB backed tools

* Fixes

* progress on autolayout improvements

* Abort fixes

* vertical insertion improvement

* Consolidate file attachments

* Fix lint

* Manage agent result card fix

* Remove hardcoded ff

* Fix file streaming

* Fix persisted writing file tab

* Fix lint

* Fix streaming file flash

* Always set url to /file on file view

* Edit perms for tables

* Fix file edit perms

* remove inline tool call json dump

* Enforce name uniqueness (#3679)

* Enforce name uniqueness

* Use established pattern for error handling

* Fix lint

* Fix lint

* Add kb name uniqueness to db

* Fix lint

* Handle name getting taken before restore

* Enforce duplicate file name

* Fix lint

---------

Co-authored-by: Theodore Li <theo@sim.ai>

* fix temp file creation

* fix types

* Streaming fixes

* type xml tag structures + return invalid id linter errors back to LLM

* Add image gen and viz tools

* Tags

* Workflow tags

* Fix lint

* Fix subagent abort

* Fix subagent persistence

* Fix subagent aborts

* Nuke db migs

* Re add db migrations

* Fix lint

---------

Co-authored-by: Theodore Li <teddy@zenobiapay.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Theodore Li <theodoreqili@gmail.com>
Co-authored-by: Theodore Li <theo@sim.ai>
2026-03-22 00:46:13 -07:00
Waleed
506d3821bd fix(auth): hide Turnstile widget container to prevent layout gap (#3706)
The invisible Turnstile iframe was taking up space between the
password field and submit button. Wrapped in a hidden div.
2026-03-21 13:23:47 -07:00
Waleed
951c8fd5e9 feat(integrations): add integrationType and tags classification to all blocks (#3702)
* feat(integrations): add integrationType and tags classification to all blocks

* improvement(integrations): replace generic api/oauth tags with use-case-oriented tags

* lint

* upgrade turbo
2026-03-21 11:45:49 -07:00
Waleed
4a34ac3015 feat(auth): add Turnstile captcha + harmony disposable email blocking (#3699)
* feat(turnstile): conditionally added CF turnstile to signup

* feat(auth): add execute-on-submit Turnstile, conditional harmony, and feature flag

- Switch Turnstile to execution: 'execute' mode so challenge runs on
  form submit (fresh token every time, no expiry issues)
- Make emailHarmony conditional via SIGNUP_EMAIL_VALIDATION_ENABLED
  feature flag so self-hosted users can opt out
- Add isSignupEmailValidationEnabled to feature-flags.ts following
  existing pattern
- Add better-auth-harmony to Next.js transpilePackages (required for
  validator.js ESM compatibility)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(validation): remove dead validateEmail and checkMXRecord

Server-side disposable email blocking is now handled by
better-auth-harmony. The async validateEmail (with MX check) had no
remaining callers. Only quickValidateEmail remains for client-side
form feedback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(auth): add 15s timeout to Turnstile captcha promise

Prevents form from hanging indefinitely if Turnstile never fires
onSuccess/onError (e.g. script fails to load, network drop).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(helm): add Turnstile and harmony env vars to values.yaml

Adds TURNSTILE_SECRET_KEY, NEXT_PUBLIC_TURNSTILE_SITE_KEY, and
SIGNUP_EMAIL_VALIDATION_ENABLED to the helm chart so self-hosted
deployments can configure captcha and disposable email blocking.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(auth): reject captcha promise on token expiry

onExpire now rejects the pending promise so the form doesn't hang
if the Turnstile token expires mid-challenge.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(login): replace useEffect keydown listener with form onSubmit

The forgot-password modal used a global window keydown listener in a
useEffect to handle Enter key — a "you might not need an effect"
anti-pattern with a stale closure risk. Replaced with a native
<form onSubmit> wrapper which handles Enter natively, eliminating
the useEffect, the global listener, and the stale closure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(auth): clear dangling timeout after captcha promise settles

Use .finally(() => clearTimeout(timeoutId)) to clean up the 15s
timeout timer when the captcha resolves before the deadline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(auth): use getResponsePromise() for Turnstile token retrieval

Replace the manual Promise + refs + timeout pattern with the
documented getResponsePromise(timeout) API from @marsidev/react-turnstile.
This eliminates captchaToken state, captchaResolveRef, captchaRejectRef,
and all callback wiring on the Turnstile component.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(auth): show captcha errors as form-level message, not password error

Captcha failures were misleadingly displayed under the password field.
Added a dedicated formError state that renders above the submit button,
making it clear the issue is with verification, not the password.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 11:23:45 -07:00
Waleed
224ff5dacc chore(trust): replace Delve trust center with Vanta (#3701)
* chore(trust): replace Delve trust center with Vanta

* lint
2026-03-21 11:05:17 -07:00
Waleed
cb3cc378b8 fix(canvas): correct z-index layering for selected blocks and connected edges (#3698)
* fix(canvas): correct z-index layering for selected blocks and connected edges

* fix(canvas): derive subflow edge z-index from connected node z-index

* fix(canvas): fix nodesForRender early-return guard for regular blocks

* lint

* fix(canvas): ensure elevated edges and last-interacted nodes sit above siblings at same base z-index
2026-03-21 10:21:13 -07:00
Waleed
a64afac075 feat(kb): harden sync engine and add connector audit logging (#3697)
* feat(kb): harden sync engine and add connector audit logging

- Fix stuck syncing status: added finally block in executeSync + stale lock recovery in cron scheduler (2hr TTL)
- Fix token expiry mid-sync: refresh OAuth token between pagination pages and before deferred content hydration
- GitHub deferred content loading: use Git blob SHA for change detection, only fetch content for new/changed docs
- Add network error keywords to isRetryableError (fetch failed, econnreset, etc.)
- Extract sanitizeStorageTitle helper to fix S3 key length limit issues
- Add audit logging for connector CRUD, sync triggers, document exclude/restore, and resource restoration paths

* lint

* fix(tests): update audit mock and route tests for new audit actions

* fix(kb): address PR review - finally block race, contentHash propagation, resourceName

- Replace DB-read finally block with local syncExitedCleanly flag to avoid race condition
- Propagate fullDoc.contentHash during deferred content hydration
- Add resourceName to file restore audit record

* fix(audit): include fileId in file restore audit description
2026-03-21 09:36:43 -07:00
Vikhyath Mondreti
e270756886 fix(kb): max depth exceeded chunks page error (#3695) 2026-03-20 15:23:44 -07:00
Adithya Krishna
6d7121110e feat(loading) show route specific skeleton UI (#3671)
* chore: fix conflicts

* chore: updated loading states
2026-03-20 12:46:24 -07:00
2102 changed files with 200101 additions and 42440 deletions

View File

@@ -0,0 +1,825 @@
---
name: add-block
description: Create or update a Sim integration block with correct subBlocks, conditions, dependsOn, modes, canonicalParamId usage, outputs, and tool wiring. Use when working on `apps/sim/blocks/blocks/{service}.ts` or aligning a block with its tools.
---
# Add Block Skill
You are an expert at creating block configurations for Sim. You understand the serializer, subBlock types, conditions, dependsOn, modes, and all UI patterns.
## Your Task
When the user asks you to create a block:
1. Create the block file in `apps/sim/blocks/blocks/{service}.ts`
2. Configure all subBlocks with proper types, conditions, and dependencies
3. Wire up tools correctly
## Block Configuration Structure
```typescript
import { {ServiceName}Icon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { getScopesForService } from '@/lib/oauth/utils'
export const {ServiceName}Block: BlockConfig = {
type: '{service}', // snake_case identifier
name: '{Service Name}', // Human readable
description: 'Brief description', // One sentence
longDescription: 'Detailed description for docs',
docsLink: 'https://docs.sim.ai/tools/{service}',
category: 'tools', // 'tools' | 'blocks' | 'triggers'
bgColor: '#HEXCOLOR', // Brand color
icon: {ServiceName}Icon,
// Auth mode
authMode: AuthMode.OAuth, // or AuthMode.ApiKey
subBlocks: [
// Define all UI fields here
],
tools: {
access: ['tool_id_1', 'tool_id_2'], // Array of tool IDs this block can use
config: {
tool: (params) => `{service}_${params.operation}`, // Tool selector function
params: (params) => ({
// Transform subBlock values to tool params
}),
},
},
inputs: {
// Optional: define expected inputs from other blocks
},
outputs: {
// Define outputs available to downstream blocks
},
}
```
## SubBlock Types Reference
**Critical:** Every subblock `id` must be unique within the block. Duplicate IDs cause conflicts even with different conditions.
### Text Inputs
```typescript
// Single-line input
{ id: 'field', title: 'Label', type: 'short-input', placeholder: '...' }
// Multi-line input
{ id: 'field', title: 'Label', type: 'long-input', placeholder: '...', rows: 6 }
// Password input
{ id: 'apiKey', title: 'API Key', type: 'short-input', password: true }
```
### Selection Inputs
```typescript
// Dropdown (static options)
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Create', id: 'create' },
{ label: 'Update', id: 'update' },
],
value: () => 'create', // Default value function
}
// Combobox (searchable dropdown)
{
id: 'field',
title: 'Label',
type: 'combobox',
options: [...],
searchable: true,
}
```
### Code/JSON Inputs
```typescript
{
id: 'code',
title: 'Code',
type: 'code',
language: 'javascript', // 'javascript' | 'json' | 'python'
placeholder: '// Enter code...',
}
```
### OAuth/Credentials
```typescript
{
id: 'credential',
title: 'Account',
type: 'oauth-input',
serviceId: '{service}', // Must match OAuth provider service key
requiredScopes: getScopesForService('{service}'), // Import from @/lib/oauth/utils
placeholder: 'Select account',
required: true,
}
```
**Scopes:** Always use `getScopesForService(serviceId)` from `@/lib/oauth/utils` for `requiredScopes`. Never hardcode scope arrays — the single source of truth is `OAUTH_PROVIDERS` in `lib/oauth/oauth.ts`.
**Scope descriptions:** When adding a new OAuth provider, also add human-readable descriptions for all scopes in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`.
### Selectors (with dynamic options)
```typescript
// Channel selector (Slack, Discord, etc.)
{
id: 'channel',
title: 'Channel',
type: 'channel-selector',
serviceId: '{service}',
placeholder: 'Select channel',
dependsOn: ['credential'],
}
// Project selector (Jira, etc.)
{
id: 'project',
title: 'Project',
type: 'project-selector',
serviceId: '{service}',
dependsOn: ['credential'],
}
// File selector (Google Drive, etc.)
{
id: 'file',
title: 'File',
type: 'file-selector',
serviceId: '{service}',
mimeType: 'application/pdf',
dependsOn: ['credential'],
}
// User selector
{
id: 'user',
title: 'User',
type: 'user-selector',
serviceId: '{service}',
dependsOn: ['credential'],
}
```
### Other Types
```typescript
// Switch/toggle
{ id: 'enabled', type: 'switch' }
// Slider
{ id: 'temperature', title: 'Temperature', type: 'slider', min: 0, max: 2, step: 0.1 }
// Table (key-value pairs)
{ id: 'headers', title: 'Headers', type: 'table', columns: ['Key', 'Value'] }
// File upload
{
id: 'files',
title: 'Attachments',
type: 'file-upload',
multiple: true,
acceptedTypes: 'image/*,application/pdf',
}
```
## File Input Handling
When your block accepts file uploads, use the basic/advanced mode pattern with `normalizeFileInput`.
### Basic/Advanced File Pattern
```typescript
// Basic mode: Visual file upload
{
id: 'uploadFile',
title: 'File',
type: 'file-upload',
canonicalParamId: 'file', // Both map to 'file' param
placeholder: 'Upload file',
mode: 'basic',
multiple: false,
required: true,
condition: { field: 'operation', value: 'upload' },
},
// Advanced mode: Reference from other blocks
{
id: 'fileRef',
title: 'File',
type: 'short-input',
canonicalParamId: 'file', // Both map to 'file' param
placeholder: 'Reference file (e.g., {{file_block.output}})',
mode: 'advanced',
required: true,
condition: { field: 'operation', value: 'upload' },
},
```
**Critical constraints:**
- `canonicalParamId` must NOT match any subblock's `id` in the same block
- Values are stored under subblock `id`, not `canonicalParamId`
### Normalizing File Input in tools.config
Use `normalizeFileInput` to handle all input variants:
```typescript
import { normalizeFileInput } from '@/blocks/utils'
tools: {
access: ['service_upload'],
config: {
tool: (params) => {
// Check all field IDs: uploadFile (basic), fileRef (advanced), fileContent (legacy)
const normalizedFile = normalizeFileInput(
params.uploadFile || params.fileRef || params.fileContent,
{ single: true }
)
if (normalizedFile) {
params.file = normalizedFile
}
return `service_${params.operation}`
},
},
}
```
**Why this pattern?**
- Values come through as `params.uploadFile` or `params.fileRef` (the subblock IDs)
- `canonicalParamId` only controls UI/schema mapping, not runtime values
- `normalizeFileInput` handles JSON strings from advanced mode template resolution
### File Input Types in `inputs`
Use `type: 'json'` for file inputs:
```typescript
inputs: {
uploadFile: { type: 'json', description: 'Uploaded file (UserFile)' },
fileRef: { type: 'json', description: 'File reference from previous block' },
// Legacy field for backwards compatibility
fileContent: { type: 'string', description: 'Legacy: base64 encoded content' },
}
```
### Multiple Files
For multiple file uploads:
```typescript
{
id: 'attachments',
title: 'Attachments',
type: 'file-upload',
multiple: true, // Allow multiple files
maxSize: 25, // Max size in MB per file
acceptedTypes: 'image/*,application/pdf,.doc,.docx',
}
// In tools.config:
const normalizedFiles = normalizeFileInput(
params.attachments || params.attachmentRefs,
// No { single: true } - returns array
)
if (normalizedFiles) {
params.files = normalizedFiles
}
```
## Condition Syntax
Controls when a field is shown based on other field values.
### Simple Condition
```typescript
condition: { field: 'operation', value: 'create' }
// Shows when operation === 'create'
```
### Multiple Values (OR)
```typescript
condition: { field: 'operation', value: ['create', 'update'] }
// Shows when operation is 'create' OR 'update'
```
### Negation
```typescript
condition: { field: 'operation', value: 'delete', not: true }
// Shows when operation !== 'delete'
```
### Compound (AND)
```typescript
condition: {
field: 'operation',
value: 'send',
and: {
field: 'type',
value: 'dm',
not: true,
}
}
// Shows when operation === 'send' AND type !== 'dm'
```
### Complex Example
```typescript
condition: {
field: 'operation',
value: ['list', 'search'],
not: true,
and: {
field: 'authMethod',
value: 'oauth',
}
}
// Shows when operation NOT in ['list', 'search'] AND authMethod === 'oauth'
```
## DependsOn Pattern
Controls when a field is enabled and when its options are refetched.
### Simple Array (all must be set)
```typescript
dependsOn: ['credential']
// Enabled only when credential has a value
// Options refetch when credential changes
dependsOn: ['credential', 'projectId']
// Enabled only when BOTH have values
```
### Complex (all + any)
```typescript
dependsOn: {
all: ['authMethod'], // All must be set
any: ['credential', 'apiKey'] // At least one must be set
}
// Enabled when authMethod is set AND (credential OR apiKey is set)
```
## Required Pattern
Can be boolean or condition-based.
### Simple Boolean
```typescript
required: true
required: false
```
### Conditional Required
```typescript
required: { field: 'operation', value: 'create' }
// Required only when operation === 'create'
required: { field: 'operation', value: ['create', 'update'] }
// Required when operation is 'create' OR 'update'
```
## Mode Pattern (Basic vs Advanced)
Controls which UI view shows the field.
### Mode Options
- `'basic'` - Only in basic view (default UI)
- `'advanced'` - Only in advanced view
- `'both'` - Both views (default if not specified)
- `'trigger'` - Only in trigger configuration
### canonicalParamId Pattern
Maps multiple UI fields to a single serialized parameter:
```typescript
// Basic mode: Visual selector
{
id: 'channel',
title: 'Channel',
type: 'channel-selector',
mode: 'basic',
canonicalParamId: 'channel', // Both map to 'channel' param
dependsOn: ['credential'],
}
// Advanced mode: Manual input
{
id: 'channelId',
title: 'Channel ID',
type: 'short-input',
mode: 'advanced',
canonicalParamId: 'channel', // Both map to 'channel' param
placeholder: 'Enter channel ID manually',
}
```
**How it works:**
- In basic mode: `channel` selector value → `params.channel`
- In advanced mode: `channelId` input value → `params.channel`
- The serializer consolidates based on current mode
**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)
- ONLY use `canonicalParamId` to link basic/advanced mode alternatives for the same logical parameter
- Do NOT use it for any other purpose
## WandConfig Pattern
Enables AI-assisted field generation.
```typescript
{
id: 'query',
title: 'Query',
type: 'code',
language: 'json',
wandConfig: {
enabled: true,
prompt: 'Generate a query based on the user request. Return ONLY the JSON.',
placeholder: 'Describe what you want to query...',
generationType: 'json-object', // Optional: affects AI behavior
maintainHistory: true, // Optional: keeps conversation context
},
}
```
### Generation Types
- `'javascript-function-body'` - JS code generation
- `'json-object'` - Raw JSON (adds "no markdown" instruction)
- `'json-schema'` - JSON Schema definitions
- `'sql-query'` - SQL statements
- `'timestamp'` - Adds current date/time context
## Tools Configuration
**Important:** `tools.config.tool` runs during serialization before variable resolution. Put `Number()` and other type coercions in `tools.config.params` instead, which runs at execution time after variables are resolved.
**Preferred:** Use tool names directly as dropdown option IDs to avoid switch cases:
```typescript
// Dropdown options use tool IDs directly
options: [
{ label: 'Create', id: 'service_create' },
{ label: 'Read', id: 'service_read' },
]
// Tool selector just returns the operation value
tool: (params) => params.operation,
```
### With Parameter Transformation
```typescript
tools: {
access: ['service_action'],
config: {
tool: (params) => 'service_action',
params: (params) => ({
id: params.resourceId,
data: typeof params.data === 'string' ? JSON.parse(params.data) : params.data,
}),
},
}
```
### V2 Versioned Tool Selector
```typescript
import { createVersionedToolSelector } from '@/blocks/utils'
tools: {
access: [
'service_create_v2',
'service_read_v2',
'service_update_v2',
],
config: {
tool: createVersionedToolSelector({
baseToolSelector: (params) => `service_${params.operation}`,
suffix: '_v2',
fallbackToolId: 'service_create_v2',
}),
},
}
```
## Outputs Definition
**IMPORTANT:** Block outputs have a simpler schema than tool outputs. Block outputs do NOT support:
- `optional: true` - This is only for tool outputs
- `items` property - This is only for tool outputs with array types
Block outputs only support:
- `type` - The data type ('string', 'number', 'boolean', 'json', 'array')
- `description` - Human readable description
- Nested object structure (for complex types)
```typescript
outputs: {
// Simple outputs
id: { type: 'string', description: 'Resource ID' },
success: { type: 'boolean', description: 'Whether operation succeeded' },
// Use type: 'json' for complex objects or arrays (NOT type: 'array' with items)
items: { type: 'json', description: 'List of items' },
metadata: { type: 'json', description: 'Response metadata' },
// Nested outputs (for structured data)
user: {
id: { type: 'string', description: 'User ID' },
name: { type: 'string', description: 'User name' },
email: { type: 'string', description: 'User email' },
},
}
```
### 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:
```typescript
outputs: {
// BAD: Opaque json with no info about what's inside
plan: { type: 'json', description: 'Zone plan information' },
// GOOD: Describe the known fields in the description
plan: {
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
## V2 Block Pattern
When creating V2 blocks (alongside legacy V1):
```typescript
// V1 Block - mark as legacy
export const ServiceBlock: BlockConfig = {
type: 'service',
name: 'Service (Legacy)',
hideFromToolbar: true, // Hide from toolbar
// ... rest of config
}
// V2 Block - visible, uses V2 tools
export const ServiceV2Block: BlockConfig = {
type: 'service_v2',
name: 'Service', // Clean name
hideFromToolbar: false, // Visible
subBlocks: ServiceBlock.subBlocks, // Reuse UI
tools: {
access: ServiceBlock.tools?.access?.map(id => `${id}_v2`) || [],
config: {
tool: createVersionedToolSelector({
baseToolSelector: (params) => (ServiceBlock.tools?.config as any)?.tool(params),
suffix: '_v2',
fallbackToolId: 'service_default_v2',
}),
params: ServiceBlock.tools?.config?.params,
},
},
outputs: {
// Flat, API-aligned outputs (not wrapped in content/metadata)
},
}
```
## Registering Blocks
After creating the block, remind the user to:
1. Import in `apps/sim/blocks/registry.ts`
2. Add to the `registry` object (alphabetically):
```typescript
import { ServiceBlock } from '@/blocks/blocks/service'
export const registry: Record<string, BlockConfig> = {
// ... existing blocks ...
service: ServiceBlock,
}
```
## Complete Example
```typescript
import { ServiceIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { getScopesForService } from '@/lib/oauth/utils'
export const ServiceBlock: BlockConfig = {
type: 'service',
name: 'Service',
description: 'Integrate with Service API',
longDescription: 'Full description for documentation...',
docsLink: 'https://docs.sim.ai/tools/service',
category: 'tools',
bgColor: '#FF6B6B',
icon: ServiceIcon,
authMode: AuthMode.OAuth,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Create', id: 'create' },
{ label: 'Read', id: 'read' },
{ label: 'Update', id: 'update' },
{ label: 'Delete', id: 'delete' },
],
value: () => 'create',
},
{
id: 'credential',
title: 'Service Account',
type: 'oauth-input',
serviceId: 'service',
requiredScopes: getScopesForService('service'),
placeholder: 'Select account',
required: true,
},
{
id: 'resourceId',
title: 'Resource ID',
type: 'short-input',
placeholder: 'Enter resource ID',
condition: { field: 'operation', value: ['read', 'update', 'delete'] },
required: { field: 'operation', value: ['read', 'update', 'delete'] },
},
{
id: 'name',
title: 'Name',
type: 'short-input',
placeholder: 'Resource name',
condition: { field: 'operation', value: ['create', 'update'] },
required: { field: 'operation', value: 'create' },
},
],
tools: {
access: ['service_create', 'service_read', 'service_update', 'service_delete'],
config: {
tool: (params) => `service_${params.operation}`,
},
},
outputs: {
id: { type: 'string', description: 'Resource ID' },
name: { type: 'string', description: 'Resource name' },
createdAt: { type: 'string', description: 'Creation timestamp' },
},
}
```
## Connecting Blocks with Triggers
If the service supports webhooks, connect the block to its triggers.
```typescript
import { getTrigger } from '@/triggers'
export const ServiceBlock: BlockConfig = {
// ... basic config ...
triggers: {
enabled: true,
available: ['service_event_a', 'service_event_b', 'service_webhook'],
},
subBlocks: [
// Tool subBlocks first...
{ id: 'operation', /* ... */ },
// Then spread trigger subBlocks
...getTrigger('service_event_a').subBlocks,
...getTrigger('service_event_b').subBlocks,
...getTrigger('service_webhook').subBlocks,
],
}
```
See the `/add-trigger` skill for creating triggers.
## Icon Requirement
If the icon doesn't already exist in `@/components/icons.tsx`, **do NOT search for it yourself**. After completing the block, ask the user to provide the SVG:
```
The block is complete, but I need an icon for {Service}.
Please provide the SVG and I'll convert it to a React component.
You can usually find this in the service's brand/press kit page, or copy it from their website.
```
## Advanced Mode for Optional Fields
Optional fields that are rarely used should be set to `mode: 'advanced'` so they don't clutter the basic UI. This includes:
- Pagination tokens
- Time range filters (start/end time)
- Sort order options
- Reply settings
- Rarely used IDs (e.g., reply-to tweet ID, quote tweet ID)
- Max results / limits
```typescript
{
id: 'startTime',
title: 'Start Time',
type: 'short-input',
placeholder: 'ISO 8601 timestamp',
condition: { field: 'operation', value: ['search', 'list'] },
mode: 'advanced', // Rarely used, hide from basic view
}
```
## WandConfig for Complex Inputs
Use `wandConfig` for fields that are hard to fill out manually, such as timestamps, comma-separated lists, and complex query strings. This gives users an AI-assisted input experience.
```typescript
// Timestamps - use generationType: 'timestamp' to inject current date context
{
id: 'startTime',
title: 'Start Time',
type: 'short-input',
mode: 'advanced',
wandConfig: {
enabled: true,
prompt: 'Generate an ISO 8601 timestamp based on the user description. Return ONLY the timestamp string.',
generationType: 'timestamp',
},
}
// Comma-separated lists - simple prompt without generationType
{
id: 'mediaIds',
title: 'Media IDs',
type: 'short-input',
mode: 'advanced',
wandConfig: {
enabled: true,
prompt: 'Generate a comma-separated list of media IDs. Return ONLY the comma-separated values.',
},
}
```
## Naming Convention
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.
## Checklist Before Finishing
- [ ] All subBlocks have `id`, `title` (except switch), and `type`
- [ ] Conditions use correct syntax (field, value, not, and)
- [ ] DependsOn set for fields that need other values
- [ ] Required fields marked correctly (boolean or condition)
- [ ] OAuth inputs have correct `serviceId` and `requiredScopes: getScopesForService(serviceId)`
- [ ] Scope descriptions added to `SCOPE_DESCRIPTIONS` in `lib/oauth/utils.ts` for any new scopes
- [ ] 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
- [ ] 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'`
- [ ] Timestamps and complex inputs have `wandConfig` enabled
## Final Validation (Required)
After creating the block, you MUST validate it against every tool it references:
1. **Read every tool definition** that appears in `tools.access` — do not skip any
2. **For each tool, verify the block has correct:**
- SubBlock inputs that cover all required tool params (with correct `condition` to show for that operation)
- SubBlock input types that match the tool param types (e.g., dropdown for enums, short-input for strings)
- `tools.config.params` correctly maps subBlock IDs to tool param names (if they differ)
- 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

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Add Block"
short_description: "Build a Sim block definition"
brand_color: "#2563EB"
default_prompt: "Use $add-block to create or update the block for a Sim integration."

View File

@@ -0,0 +1,528 @@
---
name: add-connector
description: Add or update a Sim knowledge base connector for syncing documents from an external source, including auth mode, config fields, pagination, document mapping, tags, and registry wiring. Use when working in `apps/sim/connectors/{service}/` or adding a new external document source.
---
# Add Connector Skill
You are an expert at adding knowledge base connectors to Sim. A connector syncs documents from an external source (Confluence, Google Drive, Notion, etc.) into a knowledge base.
## Your Task
When the user asks you to create a connector:
1. Use Context7 or WebFetch to read the service's API documentation
2. Determine the auth mode: **OAuth** (if Sim already has an OAuth provider for the service) or **API key** (if the service uses API key / Bearer token auth)
3. Create the connector directory and config
4. Register it in the connector registry
## Directory Structure
Create files in `apps/sim/connectors/{service}/`:
```
connectors/{service}/
├── index.ts # Barrel export
└── {service}.ts # ConnectorConfig definition
```
## Authentication
Connectors use a discriminated union for auth config (`ConnectorAuthConfig` in `connectors/types.ts`):
```typescript
type ConnectorAuthConfig =
| { mode: 'oauth'; provider: OAuthService; requiredScopes?: string[] }
| { mode: 'apiKey'; label?: string; placeholder?: string }
```
### OAuth mode
For services with existing OAuth providers in `apps/sim/lib/oauth/types.ts`. The `provider` must match an `OAuthService`. The modal shows a credential picker and handles token refresh automatically.
### API key mode
For services that use API key / Bearer token auth. The modal shows a password input with the configured `label` and `placeholder`. The API key is encrypted at rest using AES-256-GCM and stored in a dedicated `encryptedApiKey` column on the connector record. The sync engine decrypts it automatically — connectors receive the raw access token in `listDocuments`, `getDocument`, and `validateConfig`.
## ConnectorConfig Structure
### OAuth connector example
```typescript
import { createLogger } from '@sim/logger'
import { {Service}Icon } from '@/components/icons'
import { fetchWithRetry } from '@/lib/knowledge/documents/utils'
import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types'
const logger = createLogger('{Service}Connector')
export const {service}Connector: ConnectorConfig = {
id: '{service}',
name: '{Service}',
description: 'Sync documents from {Service} into your knowledge base',
version: '1.0.0',
icon: {Service}Icon,
auth: {
mode: 'oauth',
provider: '{service}', // Must match OAuthService in lib/oauth/types.ts
requiredScopes: ['read:...'],
},
configFields: [
// Rendered dynamically by the add-connector modal UI
// Supports 'short-input' and 'dropdown' types
],
listDocuments: async (accessToken, sourceConfig, cursor) => {
// Return metadata stubs with contentDeferred: true (if per-doc content fetch needed)
// Or full documents with content (if list API returns content inline)
// Return { documents: ExternalDocument[], nextCursor?, hasMore }
},
getDocument: async (accessToken, sourceConfig, externalId) => {
// Fetch full content for a single document
// Return ExternalDocument with contentDeferred: false, or null
},
validateConfig: async (accessToken, sourceConfig) => {
// Return { valid: true } or { valid: false, error: 'message' }
},
// Optional: map source metadata to semantic tag keys (translated to slots by sync engine)
mapTags: (metadata) => {
// Return Record<string, unknown> with keys matching tagDefinitions[].id
},
}
```
### API key connector example
```typescript
export const {service}Connector: ConnectorConfig = {
id: '{service}',
name: '{Service}',
description: 'Sync documents from {Service} into your knowledge base',
version: '1.0.0',
icon: {Service}Icon,
auth: {
mode: 'apiKey',
label: 'API Key', // Shown above the input field
placeholder: 'Enter your {Service} API key', // Input placeholder
},
configFields: [ /* ... */ ],
listDocuments: async (accessToken, sourceConfig, cursor) => { /* ... */ },
getDocument: async (accessToken, sourceConfig, externalId) => { /* ... */ },
validateConfig: async (accessToken, sourceConfig) => { /* ... */ },
}
```
## ConfigField Types
The add-connector modal renders these automatically — no custom UI needed.
Three field types are supported: `short-input`, `dropdown`, and `selector`.
```typescript
// Text input
{
id: 'domain',
title: 'Domain',
type: 'short-input',
placeholder: 'yoursite.example.com',
required: true,
}
// Dropdown (static options)
{
id: 'contentType',
title: 'Content Type',
type: 'dropdown',
required: false,
options: [
{ label: 'Pages only', id: 'page' },
{ label: 'Blog posts only', id: 'blogpost' },
{ label: 'All content', id: 'all' },
],
}
```
## Dynamic Selectors (Canonical Pairs)
Use `type: 'selector'` to fetch options dynamically from the existing selector registry (`hooks/selectors/registry.ts`). Selectors are always paired with a manual fallback input using the **canonical pair** pattern — a `selector` field (basic mode) and a `short-input` field (advanced mode) linked by `canonicalParamId`.
The user sees a toggle button (ArrowLeftRight) to switch between the selector dropdown and manual text input. On submit, the modal resolves each canonical pair to the active mode's value, keyed by `canonicalParamId`.
### Rules
1. **Every selector field MUST have a canonical pair** — a corresponding `short-input` (or `dropdown`) field with the same `canonicalParamId` and `mode: 'advanced'`.
2. **`required` must be set identically on both fields** in a pair. If the selector is required, the manual input must also be required.
3. **`canonicalParamId` must match the key the connector expects in `sourceConfig`** (e.g. `baseId`, `channel`, `teamId`). The advanced field's `id` should typically match `canonicalParamId`.
4. **`dependsOn` references the selector field's `id`**, not the `canonicalParamId`. The modal propagates dependency clearing across canonical siblings automatically — changing either field in a parent pair clears dependent children.
### Selector canonical pair example (Airtable base → table cascade)
```typescript
configFields: [
// Base: selector (basic) + manual (advanced)
{
id: 'baseSelector',
title: 'Base',
type: 'selector',
selectorKey: 'airtable.bases', // Must exist in hooks/selectors/registry.ts
canonicalParamId: 'baseId',
mode: 'basic',
placeholder: 'Select a base',
required: true,
},
{
id: 'baseId',
title: 'Base ID',
type: 'short-input',
canonicalParamId: 'baseId',
mode: 'advanced',
placeholder: 'e.g. appXXXXXXXXXXXXXX',
required: true,
},
// Table: selector depends on base (basic) + manual (advanced)
{
id: 'tableSelector',
title: 'Table',
type: 'selector',
selectorKey: 'airtable.tables',
canonicalParamId: 'tableIdOrName',
mode: 'basic',
dependsOn: ['baseSelector'], // References the selector field ID
placeholder: 'Select a table',
required: true,
},
{
id: 'tableIdOrName',
title: 'Table Name or ID',
type: 'short-input',
canonicalParamId: 'tableIdOrName',
mode: 'advanced',
placeholder: 'e.g. Tasks',
required: true,
},
// Non-selector fields stay as-is
{ id: 'maxRecords', title: 'Max Records', type: 'short-input', ... },
]
```
### Selector with domain dependency (Jira/Confluence pattern)
When a selector depends on a plain `short-input` field (no canonical pair), `dependsOn` references that field's `id` directly. The `domain` field's value maps to `SelectorContext.domain` automatically via `SELECTOR_CONTEXT_FIELDS`.
```typescript
configFields: [
{
id: 'domain',
title: 'Jira Domain',
type: 'short-input',
placeholder: 'yoursite.atlassian.net',
required: true,
},
{
id: 'projectSelector',
title: 'Project',
type: 'selector',
selectorKey: 'jira.projects',
canonicalParamId: 'projectKey',
mode: 'basic',
dependsOn: ['domain'],
placeholder: 'Select a project',
required: true,
},
{
id: 'projectKey',
title: 'Project Key',
type: 'short-input',
canonicalParamId: 'projectKey',
mode: 'advanced',
placeholder: 'e.g. ENG, PROJ',
required: true,
},
]
```
### How `dependsOn` maps to `SelectorContext`
The connector selector field builds a `SelectorContext` from dependency values. For the mapping to work, each dependency's `canonicalParamId` (or field `id` for non-canonical fields) must exist in `SELECTOR_CONTEXT_FIELDS` (`lib/workflows/subblocks/context.ts`):
```
oauthCredential, domain, teamId, projectId, knowledgeBaseId, planId,
siteId, collectionId, spreadsheetId, fileId, baseId, datasetId, serviceDeskId
```
### Available selector keys
Check `hooks/selectors/types.ts` for the full `SelectorKey` union. Common ones for connectors:
| SelectorKey | Context Deps | Returns |
|-------------|-------------|---------|
| `airtable.bases` | credential | Base ID + name |
| `airtable.tables` | credential, `baseId` | Table ID + name |
| `slack.channels` | credential | Channel ID + name |
| `gmail.labels` | credential | Label ID + name |
| `google.calendar` | credential | Calendar ID + name |
| `linear.teams` | credential | Team ID + name |
| `linear.projects` | credential, `teamId` | Project ID + name |
| `jira.projects` | credential, `domain` | Project key + name |
| `confluence.spaces` | credential, `domain` | Space key + name |
| `notion.databases` | credential | Database ID + name |
| `asana.workspaces` | credential | Workspace GID + name |
| `microsoft.teams` | credential | Team ID + name |
| `microsoft.channels` | credential, `teamId` | Channel ID + name |
| `webflow.sites` | credential | Site ID + name |
| `outlook.folders` | credential | Folder ID + name |
## ExternalDocument Shape
Every document returned from `listDocuments`/`getDocument` must include:
```typescript
{
externalId: string // Source-specific unique ID
title: string // Document title
content: string // Extracted plain text (or '' if contentDeferred)
contentDeferred?: boolean // true = content will be fetched via getDocument
mimeType: 'text/plain' // Always text/plain (content is extracted)
contentHash: string // Metadata-based hash for change detection
sourceUrl?: string // Link back to original (stored on document record)
metadata?: Record<string, unknown> // Source-specific data (fed to mapTags)
}
```
## Content Deferral (Required for file/content-download connectors)
**All connectors that require per-document API calls to fetch content MUST use `contentDeferred: true`.** This is the standard pattern — `listDocuments` returns lightweight metadata stubs, and content is fetched lazily by the sync engine via `getDocument` only for new/changed documents.
This pattern is critical for reliability: the sync engine processes documents in batches and enqueues each batch for processing immediately. If a sync times out, all previously-batched documents are already queued. Without deferral, content downloads during listing can exhaust the sync task's time budget before any documents are saved.
### When to use `contentDeferred: true`
- The service's list API does NOT return document content (only metadata)
- Content requires a separate download/export API call per document
- Examples: Google Drive, OneDrive, SharePoint, Dropbox, Notion, Confluence, Gmail, Obsidian, Evernote, GitHub
### When NOT to use `contentDeferred`
- The list API already returns the full content inline (e.g., Slack messages, Reddit posts, HubSpot notes)
- No per-document API call is needed to get content
### Content Hash Strategy
Use a **metadata-based** `contentHash` — never a content-based hash. The hash must be derivable from the list response metadata alone, so the sync engine can detect changes without downloading content.
Good metadata hash sources:
- `modifiedTime` / `lastModifiedDateTime` — changes when file is edited
- Git blob SHA — unique per content version
- API-provided content hash (e.g., Dropbox `content_hash`)
- Version number (e.g., Confluence page version)
Format: `{service}:{id}:{changeIndicator}`
```typescript
// Google Drive: modifiedTime changes on edit
contentHash: `gdrive:${file.id}:${file.modifiedTime ?? ''}`
// GitHub: blob SHA is a content-addressable hash
contentHash: `gitsha:${item.sha}`
// Dropbox: API provides content_hash
contentHash: `dropbox:${entry.id}:${entry.content_hash ?? entry.server_modified}`
// Confluence: version number increments on edit
contentHash: `confluence:${page.id}:${page.version.number}`
```
**Critical invariant:** The `contentHash` MUST be identical whether produced by `listDocuments` (stub) or `getDocument` (full doc). Both should use the same stub function to guarantee this.
### Implementation Pattern
```typescript
// 1. Create a stub function (sync, no API calls)
function fileToStub(file: ServiceFile): ExternalDocument {
return {
externalId: file.id,
title: file.name || 'Untitled',
content: '',
contentDeferred: true,
mimeType: 'text/plain',
sourceUrl: `https://service.com/file/${file.id}`,
contentHash: `service:${file.id}:${file.modifiedTime ?? ''}`,
metadata: { /* fields needed by mapTags */ },
}
}
// 2. listDocuments returns stubs (fast, metadata only)
listDocuments: async (accessToken, sourceConfig, cursor) => {
const response = await fetchWithRetry(listUrl, { ... })
const files = (await response.json()).files
const documents = files.map(fileToStub)
return { documents, nextCursor, hasMore }
}
// 3. getDocument fetches content and returns full doc with SAME contentHash
getDocument: async (accessToken, sourceConfig, externalId) => {
const metadata = await fetchWithRetry(metadataUrl, { ... })
const file = await metadata.json()
if (file.trashed) return null
try {
const content = await fetchContent(accessToken, file)
if (!content.trim()) return null
const stub = fileToStub(file)
return { ...stub, content, contentDeferred: false }
} catch (error) {
logger.warn(`Failed to fetch content for: ${file.name}`, { error })
return null
}
}
```
### Reference Implementations
- **Google Drive**: `connectors/google-drive/google-drive.ts` — file download/export with `modifiedTime` hash
- **GitHub**: `connectors/github/github.ts` — git blob SHA hash
- **Notion**: `connectors/notion/notion.ts` — blocks API with `last_edited_time` hash
- **Confluence**: `connectors/confluence/confluence.ts` — version number hash
## tagDefinitions — Declared Tag Definitions
Declare which tags the connector populates using semantic IDs. Shown in the add-connector modal as opt-out checkboxes.
On connector creation, slots are **dynamically assigned** via `getNextAvailableSlot` — connectors never hardcode slot names.
```typescript
tagDefinitions: [
{ id: 'labels', displayName: 'Labels', fieldType: 'text' },
{ id: 'version', displayName: 'Version', fieldType: 'number' },
{ id: 'lastModified', displayName: 'Last Modified', fieldType: 'date' },
],
```
Each entry has:
- `id`: Semantic key matching a key returned by `mapTags` (e.g. `'labels'`, `'version'`)
- `displayName`: Human-readable name shown in the UI (e.g. "Labels", "Last Modified")
- `fieldType`: `'text'` | `'number'` | `'date'` | `'boolean'` — determines which slot pool to draw from
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`.
## mapTags — Metadata to Semantic Keys
Maps source metadata to semantic tag keys. Required if `tagDefinitions` is set.
The sync engine calls this automatically and translates semantic keys to actual DB slots
using the `tagSlotMapping` stored on the connector.
Return keys must match the `id` values declared in `tagDefinitions`.
```typescript
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(', ')
// Validate numbers — guard against NaN
if (metadata.version != null) {
const num = Number(metadata.version)
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
}
return result
}
```
## External API Calls — Use `fetchWithRetry`
All external API calls must use `fetchWithRetry` from `@/lib/knowledge/documents/utils` instead of raw `fetch()`. This provides exponential backoff with retries on 429/502/503/504 errors. It returns a standard `Response` — all `.ok`, `.json()`, `.text()` checks work unchanged.
For `validateConfig` (user-facing, called on save), pass `VALIDATE_RETRY_OPTIONS` to cap wait time at ~7s. Background operations (`listDocuments`, `getDocument`) use the built-in defaults (5 retries, ~31s max).
```typescript
import { VALIDATE_RETRY_OPTIONS, fetchWithRetry } from '@/lib/knowledge/documents/utils'
// Background sync — use defaults
const response = await fetchWithRetry(url, {
method: 'GET',
headers: { Authorization: `Bearer ${accessToken}` },
})
// validateConfig — tighter retry budget
const response = await fetchWithRetry(url, { ... }, VALIDATE_RETRY_OPTIONS)
```
## sourceUrl
If `ExternalDocument.sourceUrl` is set, the sync engine stores it on the document record. Always construct the full URL (not a relative path).
## Sync Engine Behavior (Do Not Modify)
The sync engine (`lib/knowledge/connectors/sync-engine.ts`) is connector-agnostic. It:
1. Calls `listDocuments` with pagination until `hasMore` is false
2. Compares `contentHash` to detect new/changed/unchanged documents
3. Stores `sourceUrl` and calls `mapTags` on insert/update automatically
4. Handles soft-delete of removed documents
5. Resolves access tokens automatically — OAuth tokens are refreshed, API keys are decrypted from the `encryptedApiKey` column
You never need to modify the sync engine when adding a connector.
## Icon
The `icon` field on `ConnectorConfig` is used throughout the UI — in the connector list, the add-connector modal, and as the document icon in the knowledge base table (replacing the generic file type icon for connector-sourced documents). The icon is read from `CONNECTOR_REGISTRY[connectorType].icon` at runtime — no separate icon map to maintain.
If the service already has an icon in `apps/sim/components/icons.tsx` (from a tool integration), reuse it. Otherwise, ask the user to provide the SVG.
## Registering
Add one line to `apps/sim/connectors/registry.ts`:
```typescript
import { {service}Connector } from '@/connectors/{service}'
export const CONNECTOR_REGISTRY: ConnectorRegistry = {
// ... existing connectors ...
{service}: {service}Connector,
}
```
## Reference Implementations
- **OAuth + contentDeferred**: `apps/sim/connectors/google-drive/google-drive.ts` — file download with metadata-based hash, `orderBy` for deterministic pagination
- **OAuth + contentDeferred (blocks API)**: `apps/sim/connectors/notion/notion.ts` — complex block content extraction deferred to `getDocument`
- **OAuth + contentDeferred (git)**: `apps/sim/connectors/github/github.ts` — blob SHA hash, tree listing
- **OAuth + inline content**: `apps/sim/connectors/confluence/confluence.ts` — multiple config field types, `mapTags`, label fetching
- **API key**: `apps/sim/connectors/fireflies/fireflies.ts` — GraphQL API with Bearer token auth
## Checklist
- [ ] Created `connectors/{service}/{service}.ts` with full ConnectorConfig
- [ ] Created `connectors/{service}/index.ts` barrel export
- [ ] **Auth configured correctly:**
- OAuth: `auth.provider` matches an existing `OAuthService` in `lib/oauth/types.ts`
- API key: `auth.label` and `auth.placeholder` set appropriately
- [ ] **Selector fields configured correctly (if applicable):**
- Every `type: 'selector'` field has a canonical pair (`short-input` or `dropdown` with same `canonicalParamId` and `mode: 'advanced'`)
- `required` is identical on both fields in each canonical pair
- `selectorKey` exists in `hooks/selectors/registry.ts`
- `dependsOn` references selector field IDs (not `canonicalParamId`)
- Dependency `canonicalParamId` values exist in `SELECTOR_CONTEXT_FIELDS`
- [ ] `listDocuments` handles pagination with metadata-based content hashes
- [ ] `contentDeferred: true` used if content requires per-doc API calls (file download, export, blocks fetch)
- [ ] `contentHash` is metadata-based (not content-based) and identical between stub and `getDocument`
- [ ] `sourceUrl` set on each ExternalDocument (full URL, not relative)
- [ ] `metadata` includes source-specific data for tag mapping
- [ ] `tagDefinitions` declared for each semantic key returned by `mapTags`
- [ ] `mapTags` implemented if source has useful metadata (labels, dates, versions)
- [ ] `validateConfig` verifies the source is accessible
- [ ] All external API calls use `fetchWithRetry` (not raw `fetch`)
- [ ] All optional config fields validated in `validateConfig`
- [ ] Icon exists in `components/icons.tsx` (or asked user to provide SVG)
- [ ] Registered in `connectors/registry.ts`

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Add Connector"
short_description: "Build a Sim knowledge connector"
brand_color: "#0F766E"
default_prompt: "Use $add-connector to add or update a Sim knowledge connector for a service."

View File

@@ -0,0 +1,760 @@
---
name: add-integration
description: Add a complete Sim integration from API docs, covering tools, block, icon, optional triggers, registrations, and integration conventions. Use when introducing a new service under `apps/sim/tools`, `apps/sim/blocks`, and `apps/sim/triggers`.
---
# Add Integration Skill
You are an expert at adding complete integrations to Sim. This skill orchestrates the full process of adding a new service integration.
## Overview
Adding an integration involves these steps in order:
1. **Research** - Read the service's API documentation
2. **Create Tools** - Build tool configurations for each API operation
3. **Create Block** - Build the block UI configuration
4. **Add Icon** - Add the service's brand icon
5. **Create Triggers** (optional) - If the service supports webhooks
6. **Register** - Register tools, block, and triggers in their registries
7. **Generate Docs** - Run the docs generation script
## Step 1: Research the API
Before writing any code:
1. Use Context7 to find official documentation: `mcp__plugin_context7_context7__resolve-library-id`
2. Or use WebFetch to read API docs directly
3. Identify:
- Authentication method (OAuth, API Key, both)
- Available operations (CRUD, search, etc.)
- Required vs optional parameters
- Response structures
## Step 2: Create Tools
### Directory Structure
```
apps/sim/tools/{service}/
├── index.ts # Barrel exports
├── types.ts # TypeScript interfaces
├── {action1}.ts # Tool for action 1
├── {action2}.ts # Tool for action 2
└── ...
```
### Key Patterns
**types.ts:**
```typescript
import type { ToolResponse } from '@/tools/types'
export interface {Service}{Action}Params {
accessToken: string // For OAuth services
// OR
apiKey: string // For API key services
requiredParam: string
optionalParam?: string
}
export interface {Service}Response extends ToolResponse {
output: {
// Define output structure
}
}
```
**Tool file pattern:**
```typescript
export const {service}{Action}Tool: ToolConfig<Params, Response> = {
id: '{service}_{action}',
name: '{Service} {Action}',
description: '...',
version: '1.0.0',
oauth: { required: true, provider: '{service}' }, // If OAuth
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden', description: '...' },
// ... other params
},
request: { url, method, headers, body },
transformResponse: async (response) => {
const data = await response.json()
return {
success: true,
output: {
field: data.field ?? null, // Always handle nullables
},
}
},
outputs: { /* ... */ },
}
```
### Critical Rules
- `visibility: 'hidden'` for OAuth tokens
- `visibility: 'user-only'` for API keys and user credentials
- `visibility: 'user-or-llm'` for operation parameters
- Always use `?? null` for nullable API response fields
- Always use `?? []` for optional array fields
- Set `optional: true` for outputs that may not exist
- Never output raw JSON dumps - extract meaningful fields
- When using `type: 'json'` and you know the object shape, define `properties` with the inner fields so downstream consumers know the structure. Only use bare `type: 'json'` when the shape is truly dynamic
## Step 3: Create Block
### File Location
`apps/sim/blocks/blocks/{service}.ts`
### Block Structure
```typescript
import { {Service}Icon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { getScopesForService } from '@/lib/oauth/utils'
export const {Service}Block: BlockConfig = {
type: '{service}',
name: '{Service}',
description: '...',
longDescription: '...',
docsLink: 'https://docs.sim.ai/tools/{service}',
category: 'tools',
bgColor: '#HEXCOLOR',
icon: {Service}Icon,
authMode: AuthMode.OAuth, // or AuthMode.ApiKey
subBlocks: [
// Operation dropdown
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Operation 1', id: 'action1' },
{ label: 'Operation 2', id: 'action2' },
],
value: () => 'action1',
},
// Credential field
{
id: 'credential',
title: '{Service} Account',
type: 'oauth-input',
serviceId: '{service}',
requiredScopes: getScopesForService('{service}'),
required: true,
},
// Conditional fields per operation
// ...
],
tools: {
access: ['{service}_action1', '{service}_action2'],
config: {
tool: (params) => `{service}_${params.operation}`,
},
},
outputs: { /* ... */ },
}
```
### Key SubBlock Patterns
**Condition-based visibility:**
```typescript
{
id: 'resourceId',
title: 'Resource ID',
type: 'short-input',
condition: { field: 'operation', value: ['read', 'update', 'delete'] },
required: { field: 'operation', value: ['read', 'update', 'delete'] },
}
```
**DependsOn for cascading selectors:**
```typescript
{
id: 'project',
type: 'project-selector',
dependsOn: ['credential'],
},
{
id: 'issue',
type: 'file-selector',
dependsOn: ['credential', 'project'],
}
```
**Basic/Advanced mode for dual UX:**
```typescript
// Basic: Visual selector
{
id: 'channel',
type: 'channel-selector',
mode: 'basic',
canonicalParamId: 'channel',
dependsOn: ['credential'],
},
// Advanced: Manual input
{
id: 'channelId',
type: 'short-input',
mode: 'advanced',
canonicalParamId: 'channel',
}
```
**Critical Canonical Param Rules:**
- `canonicalParamId` must NOT match any subblock's `id` in the block
- `canonicalParamId` must be unique per operation/condition context
- Only use `canonicalParamId` to link basic/advanced alternatives for the same logical parameter
- `mode` only controls UI visibility, NOT serialization. Without `canonicalParamId`, both basic and advanced field values would be sent
- Every subblock `id` must be unique within the block. Duplicate IDs cause conflicts even with different conditions
- **Required consistency:** If one subblock in a canonical group has `required: true`, ALL subblocks in that group must have `required: true` (prevents bypassing validation by switching modes)
- **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)
## Step 4: Add Icon
### File Location
`apps/sim/components/icons.tsx`
### Pattern
```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 paths from user-provided SVG */}
</svg>
)
}
```
### Getting Icons
**Do NOT search for icons yourself.** At the end of implementation, ask the user to provide the SVG:
```
I've completed the integration. Before I can add the icon, please provide the SVG for {Service}.
You can usually find this in the service's brand/press kit page, or copy it from their website.
Paste the SVG code here and I'll convert it to a React component.
```
Once the user provides the SVG:
1. Extract the SVG paths/content
2. Create a React component that spreads props
3. Ensure viewBox is preserved from the original SVG
## Step 5: Create Triggers (Optional)
If the service supports webhooks, create triggers using the generic `buildTriggerSubBlocks` helper.
### Directory Structure
```
apps/sim/triggers/{service}/
├── index.ts # Barrel exports
├── utils.ts # Trigger options, setup instructions, extra fields
├── {event_a}.ts # Primary trigger (includes dropdown)
├── {event_b}.ts # Secondary triggers (no dropdown)
└── webhook.ts # Generic webhook (optional)
```
### Key Pattern
```typescript
import { buildTriggerSubBlocks } from '@/triggers'
import { {service}TriggerOptions, {service}SetupInstructions, build{Service}ExtraFields } from './utils'
// Primary trigger - includeDropdown: true
export const {service}EventATrigger: TriggerConfig = {
id: '{service}_event_a',
subBlocks: buildTriggerSubBlocks({
triggerId: '{service}_event_a',
triggerOptions: {service}TriggerOptions,
includeDropdown: true, // Only for primary trigger!
setupInstructions: {service}SetupInstructions('Event A'),
extraFields: build{Service}ExtraFields('{service}_event_a'),
}),
// ...
}
// Secondary triggers - no dropdown
export const {service}EventBTrigger: TriggerConfig = {
id: '{service}_event_b',
subBlocks: buildTriggerSubBlocks({
triggerId: '{service}_event_b',
triggerOptions: {service}TriggerOptions,
// No includeDropdown!
setupInstructions: {service}SetupInstructions('Event B'),
extraFields: build{Service}ExtraFields('{service}_event_b'),
}),
// ...
}
```
### Connect to Block
```typescript
import { getTrigger } from '@/triggers'
export const {Service}Block: BlockConfig = {
triggers: {
enabled: true,
available: ['{service}_event_a', '{service}_event_b'],
},
subBlocks: [
// Tool fields...
...getTrigger('{service}_event_a').subBlocks,
...getTrigger('{service}_event_b').subBlocks,
],
}
```
See `/add-trigger` skill for complete documentation.
## Step 6: Register Everything
### Tools Registry (`apps/sim/tools/registry.ts`)
```typescript
// Add import (alphabetically)
import {
{service}Action1Tool,
{service}Action2Tool,
} from '@/tools/{service}'
// Add to tools object (alphabetically)
export const tools: Record<string, ToolConfig> = {
// ... existing tools ...
{service}_action1: {service}Action1Tool,
{service}_action2: {service}Action2Tool,
}
```
### Block Registry (`apps/sim/blocks/registry.ts`)
```typescript
// Add import (alphabetically)
import { {Service}Block } from '@/blocks/blocks/{service}'
// Add to registry (alphabetically)
export const registry: Record<string, BlockConfig> = {
// ... existing blocks ...
{service}: {Service}Block,
}
```
### Trigger Registry (`apps/sim/triggers/registry.ts`) - If triggers exist
```typescript
// Add import (alphabetically)
import {
{service}EventATrigger,
{service}EventBTrigger,
{service}WebhookTrigger,
} from '@/triggers/{service}'
// Add to TRIGGER_REGISTRY (alphabetically)
export const TRIGGER_REGISTRY: TriggerRegistry = {
// ... existing triggers ...
{service}_event_a: {service}EventATrigger,
{service}_event_b: {service}EventBTrigger,
{service}_webhook: {service}WebhookTrigger,
}
```
## Step 7: Generate Docs
Run the documentation generator:
```bash
bun run scripts/generate-docs.ts
```
This creates `apps/docs/content/docs/en/tools/{service}.mdx`
## V2 Integration Pattern
If creating V2 versions (API-aligned outputs):
1. **V2 Tools** - Add `_v2` suffix, version `2.0.0`, flat outputs
2. **V2 Block** - Add `_v2` type, use `createVersionedToolSelector`
3. **V1 Block** - Add `(Legacy)` to name, set `hideFromToolbar: true`
4. **Registry** - Register both versions
```typescript
// In registry
{service}: {Service}Block, // V1 (legacy, hidden)
{service}_v2: {Service}V2Block, // V2 (visible)
```
## Complete Checklist
### Tools
- [ ] Created `tools/{service}/` directory
- [ ] Created `types.ts` with all interfaces
- [ ] Created tool file for each operation
- [ ] All params have correct visibility
- [ ] All nullable fields use `?? null`
- [ ] All optional outputs have `optional: true`
- [ ] Created `index.ts` barrel export
- [ ] Registered all tools in `tools/registry.ts`
### Block
- [ ] Created `blocks/blocks/{service}.ts`
- [ ] Defined operation dropdown with all operations
- [ ] Added credential field with `requiredScopes: getScopesForService('{service}')`
- [ ] Added conditional fields per operation
- [ ] Set up dependsOn for cascading selectors
- [ ] Configured tools.access with all tool IDs
- [ ] Configured tools.config.tool selector
- [ ] Defined outputs matching tool outputs
- [ ] Registered block in `blocks/registry.ts`
- [ ] If triggers: set `triggers.enabled` and `triggers.available`
- [ ] If triggers: spread trigger subBlocks with `getTrigger()`
### OAuth Scopes (if OAuth service)
- [ ] Defined scopes in `lib/oauth/oauth.ts` under `OAUTH_PROVIDERS`
- [ ] Added scope descriptions in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`
- [ ] Used `getCanonicalScopesForProvider()` in `auth.ts` (never hardcode)
- [ ] Used `getScopesForService()` in block `requiredScopes` (never hardcode)
### Icon
- [ ] Asked user to provide SVG
- [ ] Added icon to `components/icons.tsx`
- [ ] Icon spreads props correctly
### Triggers (if service supports webhooks)
- [ ] Created `triggers/{service}/` directory
- [ ] Created `utils.ts` with options, instructions, and extra fields helpers
- [ ] Primary trigger uses `includeDropdown: true`
- [ ] Secondary triggers do NOT have `includeDropdown`
- [ ] All triggers use `buildTriggerSubBlocks` helper
- [ ] Created `index.ts` barrel export
- [ ] Registered all triggers in `triggers/registry.ts`
### Docs
- [ ] Ran `bun run scripts/generate-docs.ts`
- [ ] Verified docs file created
### Final Validation (Required)
- [ ] Read every tool file and cross-referenced inputs/outputs against the API docs
- [ ] 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
## Example Command
When the user asks to add an integration:
```
User: Add a Stripe integration
You: I'll add the Stripe integration. Let me:
1. First, research the Stripe API using Context7
2. Create the tools for key operations (payments, subscriptions, etc.)
3. Create the block with operation dropdown
4. Register everything
5. Generate docs
6. Ask you for the Stripe icon SVG
[Proceed with implementation...]
[After completing steps 1-5...]
I've completed the Stripe integration. Before I can add the icon, please provide the SVG for Stripe.
You can usually find this in the service's brand/press kit page, or copy it from their website.
Paste the SVG code here and I'll convert it to a React component.
```
## File Handling
When your integration handles file uploads or downloads, follow these patterns to work with `UserFile` objects consistently.
### What is a UserFile?
A `UserFile` is the standard file representation in Sim:
```typescript
interface UserFile {
id: string // Unique identifier
name: string // Original filename
url: string // Presigned URL for download
size: number // File size in bytes
type: string // MIME type (e.g., 'application/pdf')
base64?: string // Optional base64 content (if small file)
key?: string // Internal storage key
context?: object // Storage context metadata
}
```
### File Input Pattern (Uploads)
For tools that accept file uploads, **always route through an internal API endpoint** rather than calling external APIs directly. This ensures proper file content retrieval.
#### 1. Block SubBlocks for File Input
Use the basic/advanced mode pattern:
```typescript
// Basic mode: File upload UI
{
id: 'uploadFile',
title: 'File',
type: 'file-upload',
canonicalParamId: 'file', // Maps to 'file' param
placeholder: 'Upload file',
mode: 'basic',
multiple: false,
required: true,
condition: { field: 'operation', value: 'upload' },
},
// Advanced mode: Reference from previous block
{
id: 'fileRef',
title: 'File',
type: 'short-input',
canonicalParamId: 'file', // Same canonical param
placeholder: 'Reference file (e.g., {{file_block.output}})',
mode: 'advanced',
required: true,
condition: { field: 'operation', value: 'upload' },
},
```
**Critical:** `canonicalParamId` must NOT match any subblock `id`.
#### 2. Normalize File Input in Block Config
In `tools.config.tool`, use `normalizeFileInput` to handle all input variants:
```typescript
import { normalizeFileInput } from '@/blocks/utils'
tools: {
config: {
tool: (params) => {
// Normalize file from basic (uploadFile), advanced (fileRef), or legacy (fileContent)
const normalizedFile = normalizeFileInput(
params.uploadFile || params.fileRef || params.fileContent,
{ single: true }
)
if (normalizedFile) {
params.file = normalizedFile
}
return `{service}_${params.operation}`
},
},
}
```
#### 3. Create Internal API Route
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 { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { FileInputSchema, 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) {
const requestId = generateRequestId()
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)
let fileBuffer: Buffer
let fileName: string
// Prefer UserFile input, fall back to legacy base64
if (data.file) {
const userFiles = processFilesToUserFiles([data.file as RawFileInput], requestId, logger)
if (userFiles.length === 0) {
return NextResponse.json({ success: false, error: 'Invalid file' }, { status: 400 })
}
const userFile = userFiles[0]
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
fileName = userFile.name
} else if (data.fileContent) {
// Legacy: base64 string (backwards compatibility)
fileBuffer = Buffer.from(data.fileContent, 'base64')
fileName = 'file'
} else {
return NextResponse.json({ success: false, error: 'File required' }, { status: 400 })
}
// Now call external API with fileBuffer
const response = await fetch('https://api.{service}.com/upload', {
method: 'POST',
headers: { Authorization: `Bearer ${data.accessToken}` },
body: new Uint8Array(fileBuffer), // Convert Buffer for fetch
})
// ... handle response
}
```
#### 4. Update Tool to Use Internal Route
```typescript
export const {service}UploadTool: ToolConfig<Params, Response> = {
id: '{service}_upload',
// ...
params: {
file: { type: 'file', required: false, visibility: 'user-or-llm' },
fileContent: { type: 'string', required: false, visibility: 'hidden' }, // Legacy
},
request: {
url: '/api/tools/{service}/upload', // Internal route
method: 'POST',
body: (params) => ({
accessToken: params.accessToken,
file: params.file,
fileContent: params.fileContent,
}),
},
}
```
### File Output Pattern (Downloads)
For tools that return files, use `FileToolProcessor` to store files and return `UserFile` objects.
#### In Tool transformResponse
```typescript
import { FileToolProcessor } from '@/executor/utils/file-tool-processor'
transformResponse: async (response, context) => {
const data = await response.json()
// Process file outputs to UserFile objects
const fileProcessor = new FileToolProcessor(context)
const file = await fileProcessor.processFileData({
data: data.content, // base64 or buffer
mimeType: data.mimeType,
filename: data.filename,
})
return {
success: true,
output: { file },
}
}
```
#### In API Route (for complex file handling)
```typescript
// Return file data that FileToolProcessor can handle
return NextResponse.json({
success: true,
output: {
file: {
data: base64Content,
mimeType: 'application/pdf',
filename: 'document.pdf',
},
},
})
```
### Key Helpers Reference
| 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 file Buffer from UserFile |
| `FileToolProcessor` | `@/executor/utils/file-tool-processor` | Process tool output files |
| `isUserFile` | `@/lib/core/utils/user-file` | Type guard for UserFile objects |
| `FileInputSchema` | `@/lib/uploads/utils/file-schemas` | Zod schema for file validation |
### Advanced Mode for Optional Fields
Optional fields that are rarely used should be set to `mode: 'advanced'` so they don't clutter the basic UI. Examples: pagination tokens, time range filters, sort order, max results, reply settings.
### WandConfig for Complex Inputs
Use `wandConfig` for fields that are hard to fill out manually:
- **Timestamps**: Use `generationType: 'timestamp'` to inject current date context into the AI prompt
- **JSON arrays**: Use `generationType: 'json-object'` for structured data
- **Complex queries**: Use a descriptive prompt explaining the expected format
```typescript
{
id: 'startTime',
title: 'Start Time',
type: 'short-input',
mode: 'advanced',
wandConfig: {
enabled: true,
prompt: 'Generate an ISO 8601 timestamp. Return ONLY the timestamp string.',
generationType: 'timestamp',
},
}
```
### OAuth Scopes (Centralized System)
Scopes are maintained in a single source of truth and reused everywhere:
1. **Define scopes** in `lib/oauth/oauth.ts` under `OAUTH_PROVIDERS[provider].services[service].scopes`
2. **Add descriptions** in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts` for the OAuth modal UI
3. **Reference in auth.ts** using `getCanonicalScopesForProvider(providerId)` from `@/lib/oauth/utils`
4. **Reference in blocks** using `getScopesForService(serviceId)` from `@/lib/oauth/utils`
**Never hardcode scope arrays** in `auth.ts` or block `requiredScopes`. Always import from the centralized source.
```typescript
// In auth.ts (Better Auth config)
scopes: getCanonicalScopesForProvider('{service}'),
// In block credential sub-block
requiredScopes: getScopesForService('{service}'),
```
### Common Gotchas
1. **OAuth serviceId must match** - The `serviceId` in oauth-input must match the OAuth provider configuration
2. **All tool IDs MUST be snake_case** - `stripe_create_payment`, not `stripeCreatePayment`. This applies to tool `id` fields, registry keys, `tools.access` arrays, and `tools.config.tool` return values
3. **Block type is snake_case** - `type: 'stripe'`, not `type: 'Stripe'`
4. **Alphabetical ordering** - Keep imports and registry entries alphabetically sorted
5. **Required can be conditional** - Use `required: { field: 'op', value: 'create' }` instead of always true
6. **DependsOn clears options** - When a dependency changes, selector options are refetched
7. **Never pass Buffer directly to fetch** - Convert to `new Uint8Array(buffer)` for TypeScript compatibility
8. **Always handle legacy file params** - Keep hidden `fileContent` params for backwards compatibility
9. **Optional fields use advanced mode** - Set `mode: 'advanced'` on rarely-used optional fields
10. **Complex inputs need wandConfig** - Timestamps, JSON arrays, and other hard-to-type values should have `wandConfig` enabled
11. **Never hardcode scopes** - Use `getScopesForService()` in blocks and `getCanonicalScopesForProvider()` in auth.ts
12. **Always add scope descriptions** - New scopes must have entries in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Add Integration"
short_description: "Build a full Sim integration"
brand_color: "#7C3AED"
default_prompt: "Use $add-integration to add a complete Sim integration for a service."

View File

@@ -0,0 +1,321 @@
---
name: add-tools
description: Create or update Sim tool configurations from service API docs, including typed params, request mapping, response transforms, outputs, and registry entries. Use when working in `apps/sim/tools/{service}/` or fixing tool definitions for an integration.
---
# Add Tools Skill
You are an expert at creating tool configurations for Sim integrations. Your job is to read API documentation and create properly structured tool files.
## Your Task
When the user asks you to create tools for a service:
1. Use Context7 or WebFetch to read the service's API documentation
2. Create the tools directory structure
3. Generate properly typed tool configurations
## Directory Structure
Create files in `apps/sim/tools/{service}/`:
```
tools/{service}/
├── index.ts # Barrel export
├── types.ts # Parameter & response types
└── {action}.ts # Individual tool files (one per operation)
```
## Tool Configuration Structure
Every tool MUST follow this exact structure:
```typescript
import type { {ServiceName}{Action}Params } from '@/tools/{service}/types'
import type { ToolConfig } from '@/tools/types'
interface {ServiceName}{Action}Response {
success: boolean
output: {
// Define output structure here
}
}
export const {serviceName}{Action}Tool: ToolConfig<
{ServiceName}{Action}Params,
{ServiceName}{Action}Response
> = {
id: '{service}_{action}', // snake_case, matches tool name
name: '{Service} {Action}', // Human readable
description: 'Brief description', // One sentence
version: '1.0.0',
// OAuth config (if service uses OAuth)
oauth: {
required: true,
provider: '{service}', // Must match OAuth provider ID
},
params: {
// Hidden params (system-injected, only use hidden for oauth accessToken)
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
// User-only params (credentials, api key, IDs user must provide)
someId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The ID of the resource',
},
// User-or-LLM params (everything else, can be provided by user OR computed by LLM)
query: {
type: 'string',
required: false, // Use false for optional
visibility: 'user-or-llm',
description: 'Search query',
},
},
request: {
url: (params) => `https://api.service.com/v1/resource/${params.id}`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
// Request body - only for POST/PUT/PATCH
// Trim ID fields to prevent copy-paste whitespace errors:
// userId: params.userId?.trim(),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
// Map API response to output
// Use ?? null for nullable fields
// Use ?? [] for optional arrays
},
}
},
outputs: {
// Define each output field
},
}
```
## Critical Rules for Parameters
### Visibility Options
- `'hidden'` - System-injected (OAuth tokens, internal params). User never sees.
- `'user-only'` - User must provide (credentials, api keys, account-specific IDs)
- `'user-or-llm'` - User provides OR LLM can compute (search queries, content, filters, most fall into this category)
### Parameter Types
- `'string'` - Text values
- `'number'` - Numeric values
- `'boolean'` - True/false
- `'json'` - Complex objects (NOT 'object', use 'json')
- `'file'` - Single file
- `'file[]'` - Multiple files
### Required vs Optional
- Always explicitly set `required: true` or `required: false`
- Optional params should have `required: false`
## Critical Rules for Outputs
### Output Types
- `'string'`, `'number'`, `'boolean'` - Primitives
- `'json'` - Complex objects (use this, NOT 'object')
- `'array'` - Arrays with `items` property
- `'object'` - Objects with `properties` property
### Optional Outputs
Add `optional: true` for fields that may not exist in the response:
```typescript
closedAt: {
type: 'string',
description: 'When the issue was closed',
optional: true,
},
```
### Typed JSON Outputs
When using `type: 'json'` and you know the object shape in advance, **always define the inner structure** using `properties` so downstream consumers know what fields are available:
```typescript
// BAD: Opaque json with no info about what's inside
metadata: {
type: 'json',
description: 'Response metadata',
},
// GOOD: Define the known properties
metadata: {
type: 'json',
description: 'Response metadata',
properties: {
id: { type: 'string', description: 'Unique ID' },
status: { type: 'string', description: 'Current status' },
count: { type: 'number', description: 'Total count' },
},
},
```
For arrays of objects, define the item structure:
```typescript
items: {
type: 'array',
description: 'List of items',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Item ID' },
name: { type: 'string', description: 'Item name' },
},
},
},
```
Only use bare `type: 'json'` without `properties` when the shape is truly dynamic or unknown.
## Critical Rules for transformResponse
### Handle Nullable Fields
ALWAYS use `?? null` for fields that may be undefined:
```typescript
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
id: data.id,
title: data.title,
body: data.body ?? null, // May be undefined
assignee: data.assignee ?? null, // May be undefined
labels: data.labels ?? [], // Default to empty array
closedAt: data.closed_at ?? null, // May be undefined
},
}
}
```
### Never Output Raw JSON Dumps
DON'T do this:
```typescript
output: {
data: data, // BAD - raw JSON dump
}
```
DO this instead - extract meaningful fields:
```typescript
output: {
id: data.id,
name: data.name,
status: data.status,
metadata: {
createdAt: data.created_at,
updatedAt: data.updated_at,
},
}
```
## Types File Pattern
Create `types.ts` with interfaces for all params and responses:
```typescript
import type { ToolResponse } from '@/tools/types'
// Parameter interfaces
export interface {Service}{Action}Params {
accessToken: string
requiredField: string
optionalField?: string
}
// Response interfaces (extend ToolResponse)
export interface {Service}{Action}Response extends ToolResponse {
output: {
field1: string
field2: number
optionalField?: string | null
}
}
```
## Index.ts Barrel Export Pattern
```typescript
// Export all tools
export { serviceTool1 } from './{action1}'
export { serviceTool2 } from './{action2}'
// Export types
export * from './types'
```
## Registering Tools
After creating tools, remind the user to:
1. Import tools in `apps/sim/tools/registry.ts`
2. Add to the `tools` object with snake_case keys:
```typescript
import { serviceActionTool } from '@/tools/{service}'
export const tools = {
// ... existing tools ...
{service}_{action}: serviceActionTool,
}
```
## V2 Tool Pattern
If creating V2 tools (API-aligned outputs), use `_v2` suffix:
- Tool ID: `{service}_{action}_v2`
- Variable name: `{action}V2Tool`
- Version: `'2.0.0'`
- Outputs: Flat, API-aligned (no content/metadata wrapper)
## Naming Convention
All tool IDs MUST use `snake_case`: `{service}_{action}` (e.g., `x_create_tweet`, `slack_send_message`). Never use camelCase or PascalCase for tool IDs.
## Checklist Before Finishing
- [ ] All tool IDs use snake_case
- [ ] All params have explicit `required: true` or `required: false`
- [ ] All params have appropriate `visibility`
- [ ] All nullable response fields use `?? null`
- [ ] All optional outputs have `optional: true`
- [ ] No raw JSON dumps in outputs
- [ ] Types file has all interfaces
- [ ] Index.ts exports all tools
## Final Validation (Required)
After creating all tools, you MUST validate every tool before finishing:
1. **Read every tool file** you created — do not skip any
2. **Cross-reference with the API docs** to verify:
- All required params are marked `required: true`
- All optional params are marked `required: false`
- Param types match the API (string, number, boolean, json)
- Request URL, method, headers, and body match the API spec
- `transformResponse` extracts the correct fields from the API response
- All output fields match what the API actually returns
- No fields are missing from outputs that the API provides
- No extra fields are defined in outputs that the API doesn't return
3. **Verify consistency** across tools:
- Shared types in `types.ts` match all tools that use them
- Tool IDs in the barrel export match the tool file definitions
- Error handling is consistent (error checks, meaningful messages)

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Add Tools"
short_description: "Build Sim tools from API docs"
brand_color: "#EA580C"
default_prompt: "Use $add-tools to create or update Sim tool definitions from service API docs."

View File

@@ -0,0 +1,708 @@
---
name: add-trigger
description: Create or update Sim webhook triggers using the generic trigger builder, service-specific setup instructions, outputs, and registry wiring. Use when working in `apps/sim/triggers/{service}/` or adding webhook support to an integration.
---
# Add Trigger Skill
You are an expert at creating webhook triggers for Sim. You understand the trigger system, the generic `buildTriggerSubBlocks` helper, and how triggers connect to blocks.
## Your Task
When the user asks you to create triggers for a service:
1. Research what webhook events the service supports
2. Create the trigger files using the generic builder
3. Register triggers and connect them to the block
## Directory Structure
```
apps/sim/triggers/{service}/
├── index.ts # Barrel exports
├── utils.ts # Service-specific helpers (trigger options, setup instructions, extra fields)
├── {event_a}.ts # Primary trigger (includes dropdown)
├── {event_b}.ts # Secondary trigger (no dropdown)
├── {event_c}.ts # Secondary trigger (no dropdown)
└── webhook.ts # Generic webhook trigger (optional, for "all events")
```
## Step 1: Create utils.ts
This file contains service-specific helpers used by all triggers.
```typescript
import type { SubBlockConfig } from '@/blocks/types'
import type { TriggerOutput } from '@/triggers/types'
/**
* Dropdown options for the trigger type selector.
* These appear in the primary trigger's dropdown.
*/
export const {service}TriggerOptions = [
{ label: 'Event A', id: '{service}_event_a' },
{ label: 'Event B', id: '{service}_event_b' },
{ label: 'Event C', id: '{service}_event_c' },
{ label: 'Generic Webhook (All Events)', id: '{service}_webhook' },
]
/**
* Generates HTML setup instructions for the trigger.
* Displayed to users to help them configure webhooks in the external service.
*/
export function {service}SetupInstructions(eventType: string): string {
const instructions = [
'Copy the <strong>Webhook URL</strong> above',
'Go to <strong>{Service} Settings > Webhooks</strong>',
'Click <strong>Add Webhook</strong>',
'Paste the webhook URL',
`Select the <strong>${eventType}</strong> event type`,
'Save the webhook configuration',
'Click "Save" above to activate your trigger',
]
return instructions
.map((instruction, index) =>
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
)
.join('')
}
/**
* Service-specific extra fields to add to triggers.
* These are inserted between webhookUrl and triggerSave.
*/
export function build{Service}ExtraFields(triggerId: string): SubBlockConfig[] {
return [
{
id: 'projectId',
title: 'Project ID (Optional)',
type: 'short-input',
placeholder: 'Leave empty for all projects',
description: 'Optionally filter to a specific project',
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
},
]
}
/**
* Build outputs for this trigger type.
* Outputs define what data is available to downstream blocks.
*/
export function build{Service}Outputs(): Record<string, TriggerOutput> {
return {
eventType: { type: 'string', description: 'The type of event that triggered this workflow' },
resourceId: { type: 'string', description: 'ID of the affected resource' },
timestamp: { type: 'string', description: 'When the event occurred (ISO 8601)' },
// Nested outputs for complex data
resource: {
id: { type: 'string', description: 'Resource ID' },
name: { type: 'string', description: 'Resource name' },
status: { type: 'string', description: 'Current status' },
},
webhook: { type: 'json', description: 'Full webhook payload' },
}
}
```
## Step 2: Create the Primary Trigger
The **primary trigger** is the first one listed. It MUST include `includeDropdown: true` so users can switch between trigger types.
```typescript
import { {Service}Icon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
build{Service}ExtraFields,
build{Service}Outputs,
{service}SetupInstructions,
{service}TriggerOptions,
} from '@/triggers/{service}/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* {Service} Event A Trigger
*
* This is the PRIMARY trigger - it includes the dropdown for selecting trigger type.
*/
export const {service}EventATrigger: TriggerConfig = {
id: '{service}_event_a',
name: '{Service} Event A',
provider: '{service}',
description: 'Trigger workflow when Event A occurs',
version: '1.0.0',
icon: {Service}Icon,
subBlocks: buildTriggerSubBlocks({
triggerId: '{service}_event_a',
triggerOptions: {service}TriggerOptions,
includeDropdown: true, // PRIMARY TRIGGER - includes dropdown
setupInstructions: {service}SetupInstructions('Event A'),
extraFields: build{Service}ExtraFields('{service}_event_a'),
}),
outputs: build{Service}Outputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
}
```
## Step 3: Create Secondary Triggers
Secondary triggers do NOT include the dropdown (it's already in the primary trigger).
```typescript
import { {Service}Icon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
build{Service}ExtraFields,
build{Service}Outputs,
{service}SetupInstructions,
{service}TriggerOptions,
} from '@/triggers/{service}/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* {Service} Event B Trigger
*/
export const {service}EventBTrigger: TriggerConfig = {
id: '{service}_event_b',
name: '{Service} Event B',
provider: '{service}',
description: 'Trigger workflow when Event B occurs',
version: '1.0.0',
icon: {Service}Icon,
subBlocks: buildTriggerSubBlocks({
triggerId: '{service}_event_b',
triggerOptions: {service}TriggerOptions,
// NO includeDropdown - secondary trigger
setupInstructions: {service}SetupInstructions('Event B'),
extraFields: build{Service}ExtraFields('{service}_event_b'),
}),
outputs: build{Service}Outputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
}
```
## Step 4: Create index.ts Barrel Export
```typescript
export { {service}EventATrigger } from './event_a'
export { {service}EventBTrigger } from './event_b'
export { {service}EventCTrigger } from './event_c'
export { {service}WebhookTrigger } from './webhook'
```
## Step 5: Register Triggers
### Trigger Registry (`apps/sim/triggers/registry.ts`)
```typescript
// Add import
import {
{service}EventATrigger,
{service}EventBTrigger,
{service}EventCTrigger,
{service}WebhookTrigger,
} from '@/triggers/{service}'
// Add to TRIGGER_REGISTRY
export const TRIGGER_REGISTRY: TriggerRegistry = {
// ... existing triggers ...
{service}_event_a: {service}EventATrigger,
{service}_event_b: {service}EventBTrigger,
{service}_event_c: {service}EventCTrigger,
{service}_webhook: {service}WebhookTrigger,
}
```
## Step 6: Connect Triggers to Block
In the block file (`apps/sim/blocks/blocks/{service}.ts`):
```typescript
import { {Service}Icon } from '@/components/icons'
import { getTrigger } from '@/triggers'
import type { BlockConfig } from '@/blocks/types'
export const {Service}Block: BlockConfig = {
type: '{service}',
name: '{Service}',
// ... other config ...
// Enable triggers and list available trigger IDs
triggers: {
enabled: true,
available: [
'{service}_event_a',
'{service}_event_b',
'{service}_event_c',
'{service}_webhook',
],
},
subBlocks: [
// Regular tool subBlocks first
{ id: 'operation', /* ... */ },
{ id: 'credential', /* ... */ },
// ... other tool fields ...
// Then spread ALL trigger subBlocks
...getTrigger('{service}_event_a').subBlocks,
...getTrigger('{service}_event_b').subBlocks,
...getTrigger('{service}_event_c').subBlocks,
...getTrigger('{service}_webhook').subBlocks,
],
// ... tools config ...
}
```
## Automatic Webhook Registration (Preferred)
If the service's API supports programmatic webhook creation, implement automatic webhook registration instead of requiring users to manually configure webhooks. This provides a much better user experience.
### When to Use Automatic Registration
Check the service's API documentation for endpoints like:
- `POST /webhooks` or `POST /hooks` - Create webhook
- `DELETE /webhooks/{id}` - Delete webhook
Services that support this pattern include: Grain, Lemlist, Calendly, Airtable, Webflow, Typeform, etc.
### Implementation Steps
#### 1. Add API Key to Extra Fields
Update your `build{Service}ExtraFields` function to include an API key field:
```typescript
export function build{Service}ExtraFields(triggerId: string): SubBlockConfig[] {
return [
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your {Service} API key',
description: 'Required to create the webhook in {Service}.',
password: true,
required: true,
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
},
// Other optional fields (e.g., campaign filter, project filter)
{
id: 'projectId',
title: 'Project ID (Optional)',
type: 'short-input',
placeholder: 'Leave empty for all projects',
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
},
]
}
```
#### 2. Update Setup Instructions for Automatic Creation
Change instructions to indicate automatic webhook creation:
```typescript
export function {service}SetupInstructions(eventType: string): string {
const instructions = [
'Enter your {Service} API Key above.',
'You can find your API key in {Service} at <strong>Settings > API</strong>.',
`Click <strong>"Save Configuration"</strong> to automatically create the webhook in {Service} for <strong>${eventType}</strong> events.`,
'The webhook will be automatically deleted when you remove this trigger.',
]
return instructions
.map((instruction, index) =>
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
)
.join('')
}
```
#### 3. Add Webhook Creation to API Route
In `apps/sim/app/api/webhooks/route.ts`, add provider-specific logic after the database save:
```typescript
// --- {Service} specific logic ---
if (savedWebhook && provider === '{service}') {
logger.info(`[${requestId}] {Service} provider detected. Creating webhook subscription.`)
try {
const result = await create{Service}WebhookSubscription(
{
id: savedWebhook.id,
path: savedWebhook.path,
providerConfig: savedWebhook.providerConfig,
},
requestId
)
if (result) {
// Update the webhook record with the external webhook ID
const updatedConfig = {
...(savedWebhook.providerConfig as Record<string, any>),
externalId: result.id,
}
await db
.update(webhook)
.set({
providerConfig: updatedConfig,
updatedAt: new Date(),
})
.where(eq(webhook.id, savedWebhook.id))
savedWebhook.providerConfig = updatedConfig
logger.info(`[${requestId}] Successfully created {Service} webhook`, {
externalHookId: result.id,
webhookId: savedWebhook.id,
})
}
} catch (err) {
logger.error(
`[${requestId}] Error creating {Service} webhook subscription, rolling back webhook`,
err
)
await db.delete(webhook).where(eq(webhook.id, savedWebhook.id))
return NextResponse.json(
{
error: 'Failed to create webhook in {Service}',
details: err instanceof Error ? err.message : 'Unknown error',
},
{ status: 500 }
)
}
}
// --- End {Service} specific logic ---
```
Then add the helper function at the end of the file:
```typescript
async function create{Service}WebhookSubscription(
webhookData: any,
requestId: string
): Promise<{ id: string } | undefined> {
try {
const { path, providerConfig } = webhookData
const { apiKey, triggerId, projectId } = providerConfig || {}
if (!apiKey) {
throw new Error('{Service} API Key is required.')
}
// Map trigger IDs to service event types
const eventTypeMap: Record<string, string | undefined> = {
{service}_event_a: 'eventA',
{service}_event_b: 'eventB',
{service}_webhook: undefined, // Generic - no filter
}
const eventType = eventTypeMap[triggerId]
const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}`
const requestBody: Record<string, any> = {
url: notificationUrl,
}
if (eventType) {
requestBody.eventType = eventType
}
if (projectId) {
requestBody.projectId = projectId
}
const response = await fetch('https://api.{service}.com/webhooks', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
})
const responseBody = await response.json()
if (!response.ok) {
const errorMessage = responseBody.message || 'Unknown API error'
let userFriendlyMessage = 'Failed to create webhook in {Service}'
if (response.status === 401) {
userFriendlyMessage = 'Invalid API Key. Please verify and try again.'
} else if (errorMessage) {
userFriendlyMessage = `{Service} error: ${errorMessage}`
}
throw new Error(userFriendlyMessage)
}
return { id: responseBody.id }
} catch (error: any) {
logger.error(`Exception during {Service} webhook creation`, { error: error.message })
throw error
}
}
```
#### 4. Add Webhook Deletion to Provider Subscriptions
In `apps/sim/lib/webhooks/provider-subscriptions.ts`:
1. Add a logger:
```typescript
const {service}Logger = createLogger('{Service}Webhook')
```
2. Add the delete function:
```typescript
export async function delete{Service}Webhook(webhook: any, requestId: string): Promise<void> {
try {
const config = getProviderConfig(webhook)
const apiKey = config.apiKey as string | undefined
const externalId = config.externalId as string | undefined
if (!apiKey || !externalId) {
{service}Logger.warn(`[${requestId}] Missing apiKey or externalId, skipping cleanup`)
return
}
const response = await fetch(`https://api.{service}.com/webhooks/${externalId}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${apiKey}`,
},
})
if (!response.ok && response.status !== 404) {
{service}Logger.warn(`[${requestId}] Failed to delete webhook (non-fatal): ${response.status}`)
} else {
{service}Logger.info(`[${requestId}] Successfully deleted webhook ${externalId}`)
}
} catch (error) {
{service}Logger.warn(`[${requestId}] Error deleting webhook (non-fatal)`, error)
}
}
```
3. Add to `cleanupExternalWebhook`:
```typescript
export async function cleanupExternalWebhook(...): Promise<void> {
// ... existing providers ...
} else if (webhook.provider === '{service}') {
await delete{Service}Webhook(webhook, requestId)
}
}
```
### Key Points for Automatic Registration
- **API Key visibility**: Always use `password: true` for API key fields
- **Error handling**: Roll back the database webhook if external creation fails
- **External ID storage**: Save the external webhook ID in `providerConfig.externalId`
- **Graceful cleanup**: Don't fail webhook deletion if cleanup fails (use non-fatal logging)
- **User-friendly errors**: Map HTTP status codes to helpful error messages
## The buildTriggerSubBlocks Helper
This is the generic helper from `@/triggers` that creates consistent trigger subBlocks.
### Function Signature
```typescript
interface BuildTriggerSubBlocksOptions {
triggerId: string // e.g., 'service_event_a'
triggerOptions: Array<{ label: string; id: string }> // Dropdown options
includeDropdown?: boolean // true only for primary trigger
setupInstructions: string // HTML instructions
extraFields?: SubBlockConfig[] // Service-specific fields
webhookPlaceholder?: string // Custom placeholder text
}
function buildTriggerSubBlocks(options: BuildTriggerSubBlocksOptions): SubBlockConfig[]
```
### What It Creates
The helper creates this structure:
1. **Dropdown** (only if `includeDropdown: true`) - Trigger type selector
2. **Webhook URL** - Read-only field with copy button
3. **Extra Fields** - Your service-specific fields (filters, options, etc.)
4. **Save Button** - Activates the trigger
5. **Instructions** - Setup guide for users
All fields automatically have:
- `mode: 'trigger'` - Only shown in trigger mode
- `condition: { field: 'selectedTriggerId', value: triggerId }` - Only shown when this trigger is selected
## Trigger Outputs & Webhook Input Formatting
### Important: Two Sources of Truth
There are two related but separate concerns:
1. **Trigger `outputs`** - Schema/contract defining what fields SHOULD be available. Used by UI for tag dropdown.
2. **`formatWebhookInput`** - Implementation that transforms raw webhook payload into actual data. Located in `apps/sim/lib/webhooks/utils.server.ts`.
**These MUST be aligned.** The fields returned by `formatWebhookInput` should match what's defined in trigger `outputs`. If they differ:
- Tag dropdown shows fields that don't exist (broken variable resolution)
- Or actual data has fields not shown in dropdown (users can't discover them)
### When to Add a formatWebhookInput Handler
- **Simple providers**: If the raw webhook payload structure already matches your outputs, you don't need a handler. The generic fallback returns `body` directly.
- **Complex providers**: If you need to transform, flatten, extract nested data, compute fields, or handle conditional logic, add a handler.
### Adding a Handler
In `apps/sim/lib/webhooks/utils.server.ts`, add a handler block:
```typescript
if (foundWebhook.provider === '{service}') {
// Transform raw webhook body to match trigger outputs
return {
eventType: body.type,
resourceId: body.data?.id || '',
timestamp: body.created_at,
resource: body.data,
}
}
```
**Key rules:**
- Return fields that match your trigger `outputs` definition exactly
- No wrapper objects like `webhook: { data: ... }` or `{service}: { ... }`
- No duplication (don't spread body AND add individual fields)
- Use `null` for missing optional data, not empty objects with empty strings
### Verify Alignment
Run the alignment checker:
```bash
bunx scripts/check-trigger-alignment.ts {service}
```
## Trigger Outputs
Trigger outputs use the same schema as block outputs (NOT tool outputs).
**Supported:**
- `type` and `description` for simple fields
- Nested object structure for complex data
**NOT Supported:**
- `optional: true` (tool outputs only)
- `items` property (tool outputs only)
```typescript
export function buildOutputs(): Record<string, TriggerOutput> {
return {
// Simple fields
eventType: { type: 'string', description: 'Event type' },
timestamp: { type: 'string', description: 'When it occurred' },
// Complex data - use type: 'json'
payload: { type: 'json', description: 'Full event payload' },
// Nested structure
resource: {
id: { type: 'string', description: 'Resource ID' },
name: { type: 'string', description: 'Resource name' },
},
}
}
```
## Generic Webhook Trigger Pattern
For services with many event types, create a generic webhook that accepts all events:
```typescript
export const {service}WebhookTrigger: TriggerConfig = {
id: '{service}_webhook',
name: '{Service} Webhook (All Events)',
// ...
subBlocks: buildTriggerSubBlocks({
triggerId: '{service}_webhook',
triggerOptions: {service}TriggerOptions,
setupInstructions: {service}SetupInstructions('All Events'),
extraFields: [
// Event type filter (optional)
{
id: 'eventTypes',
title: 'Event Types',
type: 'dropdown',
multiSelect: true,
options: [
{ label: 'Event A', id: 'event_a' },
{ label: 'Event B', id: 'event_b' },
],
placeholder: 'Leave empty for all events',
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: '{service}_webhook' },
},
// Plus any other service-specific fields
...build{Service}ExtraFields('{service}_webhook'),
],
}),
}
```
## Checklist Before Finishing
### Utils
- [ ] Created `{service}TriggerOptions` array with all trigger IDs
- [ ] Created `{service}SetupInstructions` function with clear steps
- [ ] Created `build{Service}ExtraFields` for service-specific fields
- [ ] Created output builders for each trigger type
### Triggers
- [ ] Primary trigger has `includeDropdown: true`
- [ ] Secondary triggers do NOT have `includeDropdown`
- [ ] All triggers use `buildTriggerSubBlocks` helper
- [ ] All triggers have proper outputs defined
- [ ] Created `index.ts` barrel export
### Registration
- [ ] All triggers imported in `triggers/registry.ts`
- [ ] All triggers added to `TRIGGER_REGISTRY`
- [ ] Block has `triggers.enabled: true`
- [ ] Block has all trigger IDs in `triggers.available`
- [ ] Block spreads all trigger subBlocks: `...getTrigger('id').subBlocks`
### Automatic Webhook Registration (if supported)
- [ ] Added API key field to `build{Service}ExtraFields` with `password: true`
- [ ] Updated setup instructions for automatic webhook creation
- [ ] Added provider-specific logic to `apps/sim/app/api/webhooks/route.ts`
- [ ] Added `create{Service}WebhookSubscription` helper function
- [ ] Added `delete{Service}Webhook` function to `provider-subscriptions.ts`
- [ ] Added provider to `cleanupExternalWebhook` function
### Webhook Input Formatting
- [ ] Added handler in `apps/sim/lib/webhooks/utils.server.ts` (if custom formatting needed)
- [ ] Handler returns fields matching trigger `outputs` exactly
- [ ] Run `bunx scripts/check-trigger-alignment.ts {service}` to verify alignment
### Testing
- [ ] Run `bun run type-check` to verify no TypeScript errors
- [ ] Restart dev server to pick up new triggers
- [ ] Test trigger UI shows correctly in the block
- [ ] Test automatic webhook creation works (if applicable)

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Add Trigger"
short_description: "Build Sim webhook triggers"
brand_color: "#DC2626"
default_prompt: "Use $add-trigger to create or update webhook triggers for a Sim integration."

View File

@@ -0,0 +1,333 @@
---
name: validate-connector
description: Audit an existing Sim knowledge base connector against the service API docs and repository conventions, then report and fix issues in auth, config fields, pagination, document mapping, tags, and registry entries. Use when validating or repairing code in `apps/sim/connectors/{service}/`.
---
# Validate Connector Skill
You are an expert auditor for Sim knowledge base connectors. Your job is to thoroughly validate that an existing connector is correct, complete, and follows all conventions.
## Your Task
When the user asks you to validate a connector:
1. Read the service's API documentation (via Context7 or WebFetch)
2. Read the connector implementation, OAuth config, and registry entries
3. Cross-reference everything against the API docs and Sim conventions
4. Report all issues found, grouped by severity (critical, warning, suggestion)
5. Fix all issues after reporting them
## Step 1: Gather All Files
Read **every** file for the connector — do not skip any:
```
apps/sim/connectors/{service}/{service}.ts # Connector implementation
apps/sim/connectors/{service}/index.ts # Barrel export
apps/sim/connectors/registry.ts # Connector registry entry
apps/sim/connectors/types.ts # ConnectorConfig interface, ExternalDocument, etc.
apps/sim/connectors/utils.ts # Shared utilities (computeContentHash, htmlToPlainText, etc.)
apps/sim/lib/oauth/oauth.ts # OAUTH_PROVIDERS — single source of truth for scopes
apps/sim/lib/oauth/utils.ts # getCanonicalScopesForProvider, getScopesForService, SCOPE_DESCRIPTIONS
apps/sim/lib/oauth/types.ts # OAuthService union type
apps/sim/components/icons.tsx # Icon definition for the service
```
If the connector uses selectors, also read:
```
apps/sim/hooks/selectors/registry.ts # Selector key definitions
apps/sim/hooks/selectors/types.ts # SelectorKey union type
apps/sim/lib/workflows/subblocks/context.ts # SELECTOR_CONTEXT_FIELDS
```
## Step 2: Pull API Documentation
Fetch the official API docs for the service. This is the **source of truth** for:
- Endpoint URLs, HTTP methods, and auth headers
- Required vs optional parameters
- Parameter types and allowed values
- Response shapes and field names
- Pagination patterns (cursor, offset, next token)
- Rate limits and error formats
- OAuth scopes and their meanings
Use Context7 (resolve-library-id → query-docs) or WebFetch to retrieve documentation. If both fail, note which claims are based on training knowledge vs verified docs.
## Step 3: Validate API Endpoints
For **every** API call in the connector (`listDocuments`, `getDocument`, `validateConfig`, and any helper functions), verify against the API docs:
### URLs and Methods
- [ ] Base URL is correct for the service's API version
- [ ] Endpoint paths match the API docs exactly
- [ ] HTTP method is correct (GET, POST, PUT, PATCH, DELETE)
- [ ] Path parameters are correctly interpolated and URI-encoded where needed
- [ ] Query parameters use correct names and formats per the API docs
### Headers
- [ ] Authorization header uses the correct format:
- OAuth: `Authorization: Bearer ${accessToken}`
- API Key: correct header name per the service's docs
- [ ] `Content-Type` is set for POST/PUT/PATCH requests
- [ ] Any service-specific headers are present (e.g., `Notion-Version`, `Dropbox-API-Arg`)
- [ ] No headers are sent that the API doesn't support or silently ignores
### Request Bodies
- [ ] POST/PUT body fields match API parameter names exactly
- [ ] Required fields are always sent
- [ ] Optional fields are conditionally included (not sent as `null` or empty unless the API expects that)
- [ ] Field value types match API expectations (string vs number vs boolean)
### Input Sanitization
- [ ] User-controlled values interpolated into query strings are properly escaped:
- OData `$filter`: single quotes escaped with `''` (e.g., `externalId.replace(/'/g, "''")`)
- SOQL: single quotes escaped with `\'`
- GraphQL variables: passed as variables, not interpolated into query strings
- URL path segments: `encodeURIComponent()` applied
- [ ] URL-type config fields (e.g., `siteUrl`, `instanceUrl`) are normalized:
- Strip `https://` / `http://` prefix if the API expects bare domains
- Strip trailing `/`
- Apply `.trim()` before validation
### Response Parsing
- [ ] Response structure is correctly traversed (e.g., `data.results` vs `data.items` vs `data`)
- [ ] Field names extracted match what the API actually returns
- [ ] Nullable fields are handled with `?? null` or `|| undefined`
- [ ] Error responses are checked before accessing data fields
## Step 4: Validate OAuth Scopes (if OAuth connector)
Scopes must be correctly declared and sufficient for all API calls the connector makes.
### Connector requiredScopes
- [ ] `requiredScopes` in the connector's `auth` config lists all scopes needed by the connector
- [ ] Each scope in `requiredScopes` is a real, valid scope recognized by the service's API
- [ ] No invalid, deprecated, or made-up scopes are listed
- [ ] No unnecessary excess scopes beyond what the connector actually needs
### Scope Subset Validation (CRITICAL)
- [ ] Every scope in `requiredScopes` exists in the OAuth provider's `scopes` array in `lib/oauth/oauth.ts`
- [ ] Find the provider in `OAUTH_PROVIDERS[providerGroup].services[serviceId].scopes`
- [ ] Verify: `requiredScopes``OAUTH_PROVIDERS scopes` (every required scope is present in the provider config)
- [ ] If a required scope is NOT in the provider config, flag as **critical** — the connector will fail at runtime
### Scope Sufficiency
For each API endpoint the connector calls:
- [ ] Identify which scopes are required per the API docs
- [ ] Verify those scopes are included in the connector's `requiredScopes`
- [ ] If the connector calls endpoints requiring scopes not in `requiredScopes`, flag as **warning**
### Token Refresh Config
- [ ] Check the `getOAuthTokenRefreshConfig` function in `lib/oauth/oauth.ts` for this provider
- [ ] `useBasicAuth` matches the service's token exchange requirements
- [ ] `supportsRefreshTokenRotation` matches whether the service issues rotating refresh tokens
- [ ] Token endpoint URL is correct
## Step 5: Validate Pagination
### listDocuments Pagination
- [ ] Cursor/pagination parameter name matches the API docs
- [ ] Response pagination field is correctly extracted (e.g., `next_cursor`, `nextPageToken`, `@odata.nextLink`, `offset`)
- [ ] `hasMore` is correctly determined from the response
- [ ] `nextCursor` is correctly passed back for the next page
- [ ] `maxItems` / `maxRecords` cap is correctly applied across pages using `syncContext.totalDocsFetched`
- [ ] Page size is within the API's allowed range (not exceeding max page size)
- [ ] Last page precision: when a `maxItems` cap exists, the final page request uses `Math.min(PAGE_SIZE, remaining)` to avoid fetching more records than needed
- [ ] No off-by-one errors in pagination tracking
- [ ] The connector does NOT hit known API pagination limits silently (e.g., HubSpot search 10k cap)
### Pagination State Across Pages
- [ ] `syncContext` is used to cache state across pages (user names, field maps, instance URLs, portal IDs, etc.)
- [ ] Cached state in `syncContext` is correctly initialized on first page and reused on subsequent pages
## Step 6: Validate Data Transformation
### Content Deferral (CRITICAL)
Connectors that require per-document API calls to fetch content (file download, export, blocks fetch) MUST use `contentDeferred: true`. This is the standard pattern for reliability — without it, content downloads during listing can exhaust the sync task's time budget before any documents are saved.
- [ ] If the connector downloads content per-doc during `listDocuments`, it MUST use `contentDeferred: true` instead
- [ ] `listDocuments` returns lightweight stubs with `content: ''` and `contentDeferred: true`
- [ ] `getDocument` fetches actual content and returns the full document with `contentDeferred: false`
- [ ] A shared stub function (e.g., `fileToStub`) is used by both `listDocuments` and `getDocument` to guarantee `contentHash` consistency
- [ ] `contentHash` is metadata-based (e.g., `service:{id}:{modifiedTime}`), NOT content-based — it must be derivable from list metadata alone
- [ ] The `contentHash` is identical whether produced by `listDocuments` or `getDocument`
Connectors where the list API already returns content inline (e.g., Slack messages, Reddit posts) do NOT need `contentDeferred`.
### ExternalDocument Construction
- [ ] `externalId` is a stable, unique identifier from the source API
- [ ] `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` uses a metadata-based format (e.g., `service:{id}:{modifiedTime}`) for connectors with `contentDeferred: true`, or `computeContentHash` from `@/connectors/utils` for inline-content connectors
- [ ] `sourceUrl` is a valid, complete URL back to the original resource (not relative)
- [ ] `metadata` contains all fields referenced by `mapTags` and `tagDefinitions`
### Content Extraction
- [ ] Rich text / HTML fields are converted to plain text before indexing
- [ ] Important content is not silently dropped (e.g., nested blocks, table cells, code blocks)
- [ ] Content is not silently truncated without logging a warning
- [ ] Empty/blank documents are properly filtered out
- [ ] Size checks use `Buffer.byteLength(text, 'utf8')` not `text.length` when comparing against byte-based limits (e.g., `MAX_FILE_SIZE` in bytes)
## Step 7: Validate Tag Definitions and mapTags
### tagDefinitions
- [ ] Each `tagDefinition` has an `id`, `displayName`, and `fieldType`
- [ ] `fieldType` matches the actual data type: `'text'` for strings, `'number'` for numbers, `'date'` for dates, `'boolean'` for booleans
- [ ] Every `id` in `tagDefinitions` is returned by `mapTags`
- [ ] No `tagDefinition` references a field that `mapTags` never produces
### mapTags
- [ ] Return keys match `tagDefinition` `id` values exactly
- [ ] Date values are properly parsed using `parseTagDate` from `@/connectors/utils`
- [ ] Array values are properly joined using `joinTagArray` from `@/connectors/utils`
- [ ] Number values are validated (not `NaN`)
- [ ] Metadata field names accessed in `mapTags` match what `listDocuments`/`getDocument` store in `metadata`
## Step 8: Validate Config Fields and Validation
### configFields
- [ ] Every field has `id`, `title`, `type`
- [ ] `required` is set explicitly (not omitted)
- [ ] Dropdown fields have `options` with `label` and `id` for each option
- [ ] Selector fields follow the canonical pair pattern:
- A `type: 'selector'` field with `selectorKey`, `canonicalParamId`, `mode: 'basic'`
- A `type: 'short-input'` field with the same `canonicalParamId`, `mode: 'advanced'`
- `required` is identical on both fields in the pair
- [ ] `selectorKey` values exist in the selector registry
- [ ] `dependsOn` references selector field `id` values, not `canonicalParamId`
### validateConfig
- [ ] Validates all required fields are present before making API calls
- [ ] Validates optional numeric fields (checks `Number.isNaN`, positive values)
- [ ] Makes a lightweight API call to verify access (e.g., fetch 1 record, get profile)
- [ ] Uses `VALIDATE_RETRY_OPTIONS` for retry budget
- [ ] Returns `{ valid: true }` on success
- [ ] Returns `{ valid: false, error: 'descriptive message' }` on failure
- [ ] Catches exceptions and returns user-friendly error messages
- [ ] Does NOT make expensive calls (full data listing, large queries)
## Step 9: Validate getDocument
- [ ] Fetches a single document by `externalId`
- [ ] Returns `null` for 404 / not found (does not throw)
- [ ] Returns the same `ExternalDocument` shape as `listDocuments`
- [ ] If `listDocuments` uses `contentDeferred: true`, `getDocument` MUST fetch actual content and return `contentDeferred: false`
- [ ] If `listDocuments` uses `contentDeferred: true`, `getDocument` MUST use the same stub function to ensure `contentHash` is identical
- [ ] Handles all content types that `listDocuments` can produce (e.g., if `listDocuments` returns both pages and blogposts, `getDocument` must handle both — not hardcode one endpoint)
- [ ] Forwards `syncContext` if it needs cached state (user names, field maps, etc.)
- [ ] Error handling is graceful (catches, logs, returns null or throws with context)
- [ ] Does not redundantly re-fetch data already included in the initial API response (e.g., if comments come back with the post, don't fetch them again separately)
## Step 10: Validate General Quality
### fetchWithRetry Usage
- [ ] All external API calls use `fetchWithRetry` from `@/lib/knowledge/documents/utils`
- [ ] No raw `fetch()` calls to external APIs
- [ ] `VALIDATE_RETRY_OPTIONS` used in `validateConfig`
- [ ] If `validateConfig` calls a shared helper (e.g., `linearGraphQL`, `resolveId`), that helper must accept and forward `retryOptions` to `fetchWithRetry`
- [ ] Default retry options used in `listDocuments`/`getDocument`
### API Efficiency
- [ ] APIs that support field selection (e.g., `$select`, `sysparm_fields`, `fields`) should request only the fields the connector needs — in both `listDocuments` AND `getDocument`
- [ ] No redundant API calls: if a helper already fetches data (e.g., site metadata), callers should reuse the result instead of making a second call for the same information
- [ ] Sequential per-item API calls (fetching details for each document in a loop) should be batched with `Promise.all` and a concurrency limit of 3-5
### Error Handling
- [ ] Individual document failures are caught and logged without aborting the sync
- [ ] API error responses include status codes in error messages
- [ ] No unhandled promise rejections in concurrent operations
### Concurrency
- [ ] Concurrent API calls use reasonable batch sizes (3-5 is typical)
- [ ] No unbounded `Promise.all` over large arrays
### Logging
- [ ] Uses `createLogger` from `@sim/logger` (not `console.log`)
- [ ] Logs sync progress at `info` level
- [ ] Logs errors at `warn` or `error` level with context
### Registry
- [ ] Connector is exported from `connectors/{service}/index.ts`
- [ ] Connector is registered in `connectors/registry.ts`
- [ ] Registry key matches the connector's `id` field
## Step 11: Report and Fix
### Report Format
Group findings by severity:
**Critical** (will cause runtime errors, data loss, or auth failures):
- Wrong API endpoint URL or HTTP method
- Invalid or missing OAuth scopes (not in provider config)
- Incorrect response field mapping (accessing wrong path)
- SOQL/query fields that don't exist on the target object
- Pagination that silently hits undocumented API limits
- Missing error handling that would crash the sync
- `requiredScopes` not a subset of OAuth provider scopes
- Query/filter injection: user-controlled values interpolated into OData `$filter`, SOQL, or query strings without escaping
- Per-document content download in `listDocuments` without `contentDeferred: true` — causes sync timeouts for large document sets
- `contentHash` mismatch between `listDocuments` stub and `getDocument` return — causes unnecessary re-processing every sync
**Warning** (incorrect behavior, data quality issues, or convention violations):
- HTML content not stripped via `htmlToPlainText`
- `getDocument` not forwarding `syncContext`
- `getDocument` hardcoded to one content type when `listDocuments` returns multiple (e.g., only pages but not blogposts)
- Missing `tagDefinition` for metadata fields returned by `mapTags`
- Incorrect `useBasicAuth` or `supportsRefreshTokenRotation` in token refresh config
- 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
- 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`
**Suggestion** (minor improvements):
- Missing incremental sync support despite API supporting it
- Overly broad scopes that could be narrowed (not wrong, but could be tighter)
- Source URL format could be more specific
- Missing `orderBy` for deterministic pagination
- Redundant API calls that could be cached in `syncContext`
- Sequential per-item API calls that could be batched with `Promise.all` (concurrency 3-5)
- API supports field selection but connector fetches all fields (e.g., missing `$select`, `sysparm_fields`, `fields`)
- `getDocument` re-fetches data already included in the initial API response (e.g., comments returned with post)
- Last page of pagination requests full `PAGE_SIZE` when fewer records remain (`Math.min(PAGE_SIZE, remaining)`)
### Fix All Issues
After reporting, fix every **critical** and **warning** issue. Apply **suggestions** where they don't add unnecessary complexity.
### Validation Output
After fixing, confirm:
1. `bun run lint` passes
2. TypeScript compiles clean
3. Re-read all modified files to verify fixes are correct
## Checklist Summary
- [ ] Read connector implementation, types, utils, registry, and OAuth config
- [ ] Pulled and read official API documentation for the service
- [ ] Validated every API endpoint URL, method, headers, and body against API docs
- [ ] Validated input sanitization: no query/filter injection, URL fields normalized
- [ ] Validated OAuth scopes: `requiredScopes` ⊆ OAuth provider `scopes` in `oauth.ts`
- [ ] Validated each scope is real and recognized by the service's API
- [ ] Validated scopes are sufficient for all API endpoints the connector calls
- [ ] Validated token refresh config (`useBasicAuth`, `supportsRefreshTokenRotation`)
- [ ] Validated pagination: cursor names, page sizes, hasMore logic, no silent caps
- [ ] Validated content deferral: `contentDeferred: true` used when per-doc content fetch required, metadata-based `contentHash` consistent between stub and `getDocument`
- [ ] Validated data transformation: plain text extraction, HTML stripping, content hashing
- [ ] Validated tag definitions match mapTags output, correct fieldTypes
- [ ] Validated config fields: canonical pairs, selector keys, required flags
- [ ] Validated validateConfig: lightweight check, error messages, retry options
- [ ] Validated getDocument: null on 404, all content types handled, no redundant re-fetches, syncContext forwarding
- [ ] Validated fetchWithRetry used for all external calls (no raw fetch), VALIDATE_RETRY_OPTIONS threaded through helpers
- [ ] Validated API efficiency: field selection used, no redundant calls, sequential fetches batched
- [ ] Validated error handling: graceful failures, no unhandled rejections
- [ ] Validated logging: createLogger, no console.log
- [ ] Validated registry: correct export, correct key
- [ ] Reported all issues grouped by severity
- [ ] Fixed all critical and warning issues
- [ ] Ran `bun run lint` after fixes
- [ ] Verified TypeScript compiles clean

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Validate Connector"
short_description: "Audit a Sim knowledge connector"
brand_color: "#059669"
default_prompt: "Use $validate-connector to audit and fix a Sim knowledge connector against its API docs."

View File

@@ -0,0 +1,289 @@
---
name: validate-integration
description: Audit an existing Sim integration against the service API docs and repository conventions, then report and fix issues across tools, blocks, outputs, OAuth scopes, triggers, and registry entries. Use when validating or repairing a service integration under `apps/sim/tools`, `apps/sim/blocks`, or `apps/sim/triggers`.
---
# Validate Integration Skill
You are an expert auditor for Sim integrations. Your job is to thoroughly validate that an existing integration is correct, complete, and follows all conventions.
## Your Task
When the user asks you to validate an integration:
1. Read the service's API documentation (via WebFetch or Context7)
2. Read every tool, the block, and registry entries
3. Cross-reference everything against the API docs and Sim conventions
4. Report all issues found, grouped by severity (critical, warning, suggestion)
5. Fix all issues after reporting them
## Step 1: Gather All Files
Read **every** file for the integration — do not skip any:
```
apps/sim/tools/{service}/ # All tool files, types.ts, index.ts
apps/sim/blocks/blocks/{service}.ts # Block definition
apps/sim/tools/registry.ts # Tool registry entries for this service
apps/sim/blocks/registry.ts # Block registry entry for this service
apps/sim/components/icons.tsx # Icon definition
apps/sim/lib/auth/auth.ts # OAuth config — should use getCanonicalScopesForProvider()
apps/sim/lib/oauth/oauth.ts # OAuth provider config — single source of truth for scopes
apps/sim/lib/oauth/utils.ts # Scope utilities, SCOPE_DESCRIPTIONS for modal UI
```
## Step 2: Pull API Documentation
Fetch the official API docs for the service. This is the **source of truth** for:
- Endpoint URLs, HTTP methods, and auth headers
- Required vs optional parameters
- Parameter types and allowed values
- Response shapes and field names
- Pagination patterns (which param name, which response field)
- Rate limits and error formats
## Step 3: Validate Tools
For **every** tool file, check:
### Tool ID and Naming
- [ ] Tool ID uses `snake_case`: `{service}_{action}` (e.g., `x_create_tweet`, `slack_send_message`)
- [ ] Tool `name` is human-readable (e.g., `'X Create Tweet'`)
- [ ] Tool `description` is a concise one-liner describing what it does
- [ ] Tool `version` is set (`'1.0.0'` or `'2.0.0'` for V2)
### Params
- [ ] All required API params are marked `required: true`
- [ ] All optional API params are marked `required: false`
- [ ] Every param has explicit `required: true` or `required: false` — never omitted
- [ ] Param types match the API (`'string'`, `'number'`, `'boolean'`, `'json'`)
- [ ] Visibility is correct:
- `'hidden'` — ONLY for OAuth access tokens and system-injected params
- `'user-only'` — for API keys, credentials, and account-specific IDs the user must provide
- `'user-or-llm'` — for everything else (search queries, content, filters, IDs that could come from other blocks)
- [ ] Every param has a `description` that explains what it does
### Request
- [ ] URL matches the API endpoint exactly (correct base URL, path segments, path params)
- [ ] HTTP method matches the API spec (GET, POST, PUT, PATCH, DELETE)
- [ ] Headers include correct auth pattern:
- OAuth: `Authorization: Bearer ${params.accessToken}`
- API Key: correct header name and format per the service's docs
- [ ] `Content-Type` header is set for POST/PUT/PATCH requests
- [ ] Body sends all required fields and only includes optional fields when provided
- [ ] For GET requests with query params: URL is constructed correctly with query string
- [ ] ID fields in URL paths are `.trim()`-ed to prevent copy-paste whitespace errors
- [ ] Path params use template literals correctly: `` `https://api.service.com/v1/${params.id.trim()}` ``
### Response / transformResponse
- [ ] Correctly parses the API response (`await response.json()`)
- [ ] Extracts the right fields from the response structure (e.g., `data.data` vs `data` vs `data.results`)
- [ ] All nullable fields use `?? null`
- [ ] All optional arrays use `?? []`
- [ ] Error cases are handled: checks for missing/empty data and returns meaningful error
- [ ] Does NOT do raw JSON dumps — extracts meaningful, individual fields
### Outputs
- [ ] All output fields match what the API actually returns
- [ ] No fields are missing that the API provides and users would commonly need
- [ ] No phantom fields defined that the API doesn't return
- [ ] `optional: true` is set on fields that may not exist in all responses
- [ ] When using `type: 'json'` and the shape is known, `properties` defines the inner fields
- [ ] When using `type: 'array'`, `items` defines the item structure with `properties`
- [ ] Field descriptions are accurate and helpful
### Types (types.ts)
- [ ] Has param interfaces for every tool (e.g., `XCreateTweetParams`)
- [ ] Has response interfaces for every tool (extending `ToolResponse`)
- [ ] Optional params use `?` in the interface (e.g., `replyTo?: string`)
- [ ] Field names in types match actual API field names
- [ ] Shared response types are properly reused (e.g., `XTweetResponse` shared across tweet tools)
### Barrel Export (index.ts)
- [ ] Every tool is exported
- [ ] All types are re-exported (`export * from './types'`)
- [ ] No orphaned exports (tools that don't exist)
### Tool Registry (tools/registry.ts)
- [ ] Every tool is imported and registered
- [ ] Registry keys use snake_case and match tool IDs exactly
- [ ] Entries are in alphabetical order within the file
## Step 4: Validate Block
### Block ↔ Tool Alignment (CRITICAL)
This is the most important validation — the block must be perfectly aligned with every tool it references.
For **each tool** in `tools.access`:
- [ ] The operation dropdown has an option whose ID matches the tool ID (or the `tools.config.tool` function correctly maps to it)
- [ ] Every **required** tool param (except `accessToken`) has a corresponding subBlock input that is:
- Shown when that operation is selected (correct `condition`)
- Marked as `required: true` (or conditionally required)
- [ ] Every **optional** tool param has a corresponding subBlock input (or is intentionally omitted if truly never needed)
- [ ] SubBlock `id` values are unique across the entire block — no duplicates even across different conditions
- [ ] The `tools.config.tool` function returns the correct tool ID for every possible operation value
- [ ] The `tools.config.params` function correctly maps subBlock IDs to tool param names when they differ
### SubBlocks
- [ ] Operation dropdown lists ALL tool operations available in `tools.access`
- [ ] Dropdown option labels are human-readable and descriptive
- [ ] Conditions use correct syntax:
- Single value: `{ field: 'operation', value: 'x_create_tweet' }`
- Multiple values (OR): `{ field: 'operation', value: ['x_create_tweet', 'x_delete_tweet'] }`
- Negation: `{ field: 'operation', value: 'delete', not: true }`
- Compound: `{ field: 'op', value: 'send', and: { field: 'type', value: 'dm' } }`
- [ ] Condition arrays include ALL operations that use that field — none missing
- [ ] `dependsOn` is set for fields that need other values (selectors depending on credential, cascading dropdowns)
- [ ] SubBlock types match tool param types:
- Enum/fixed options → `dropdown`
- Free text → `short-input`
- Long text/content → `long-input`
- True/false → `dropdown` with Yes/No options (not `switch` unless purely UI toggle)
- Credentials → `oauth-input` with correct `serviceId`
- [ ] Dropdown `value: () => 'default'` is set for dropdowns with a sensible default
### Advanced Mode
- [ ] Optional, rarely-used fields are set to `mode: 'advanced'`:
- Pagination tokens / next tokens
- Time range filters (start/end time)
- Sort order / direction options
- Max results / per page limits
- Reply settings / threading options
- Rarely used IDs (reply-to, quote-tweet, etc.)
- Exclude filters
- [ ] **Required** fields are NEVER set to `mode: 'advanced'`
- [ ] Fields that users fill in most of the time are NOT set to `mode: 'advanced'`
### WandConfig
- [ ] Timestamp fields have `wandConfig` with `generationType: 'timestamp'`
- [ ] Comma-separated list fields have `wandConfig` with a descriptive prompt
- [ ] Complex filter/query fields have `wandConfig` with format examples in the prompt
- [ ] All `wandConfig` prompts end with "Return ONLY the [format] - no explanations, no extra text."
- [ ] `wandConfig.placeholder` describes what to type in natural language
### Tools Config
- [ ] `tools.access` lists **every** tool ID the block can use — none missing
- [ ] `tools.config.tool` returns the correct tool ID for each operation
- [ ] Type coercions are in `tools.config.params` (runs at execution time), NOT in `tools.config.tool` (runs at serialization time before variable resolution)
- [ ] `tools.config.params` handles:
- `Number()` conversion for numeric params that come as strings from inputs
- `Boolean` / string-to-boolean conversion for toggle params
- Empty string → `undefined` conversion for optional dropdown values
- Any subBlock ID → tool param name remapping
- [ ] No `Number()`, `JSON.parse()`, or other coercions in `tools.config.tool` — these would destroy dynamic references like `<Block.output>`
### Block Outputs
- [ ] Outputs cover the key fields returned by ALL tools (not just one operation)
- [ ] Output types are correct (`'string'`, `'number'`, `'boolean'`, `'json'`)
- [ ] `type: 'json'` outputs either:
- Describe inner fields in the description string (GOOD): `'User profile (id, name, username, bio)'`
- Use nested output definitions (BEST): `{ id: { type: 'string' }, name: { type: 'string' } }`
- [ ] No opaque `type: 'json'` with vague descriptions like `'Response data'`
- [ ] Outputs that only appear for certain operations use `condition` if supported, or document which operations return them
### Block Metadata
- [ ] `type` is snake_case (e.g., `'x'`, `'cloudflare'`)
- [ ] `name` is human-readable (e.g., `'X'`, `'Cloudflare'`)
- [ ] `description` is a concise one-liner
- [ ] `longDescription` provides detail for docs
- [ ] `docsLink` points to `'https://docs.sim.ai/tools/{service}'`
- [ ] `category` is `'tools'`
- [ ] `bgColor` uses the service's brand color hex
- [ ] `icon` references the correct icon component from `@/components/icons`
- [ ] `authMode` is set correctly (`AuthMode.OAuth` or `AuthMode.ApiKey`)
- [ ] Block is registered in `blocks/registry.ts` alphabetically
### Block Inputs
- [ ] `inputs` section lists all subBlock params that the block accepts
- [ ] Input types match the subBlock types
- [ ] When using `canonicalParamId`, inputs list the canonical ID (not the raw subBlock IDs)
## Step 5: Validate OAuth Scopes (if OAuth service)
Scopes are centralized — the single source of truth is `OAUTH_PROVIDERS` in `lib/oauth/oauth.ts`.
- [ ] Scopes defined in `lib/oauth/oauth.ts` under `OAUTH_PROVIDERS[provider].services[service].scopes`
- [ ] `auth.ts` uses `getCanonicalScopesForProvider(providerId)` — NOT a hardcoded array
- [ ] Block `requiredScopes` uses `getScopesForService(serviceId)` — NOT a hardcoded array
- [ ] No hardcoded scope arrays in `auth.ts` or block files (should all use utility functions)
- [ ] Each scope has a human-readable description in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`
- [ ] No excess scopes that aren't needed by any tool
## Step 6: Validate Pagination Consistency
If any tools support pagination:
- [ ] Pagination param names match the API docs (e.g., `pagination_token` vs `next_token` vs `cursor`)
- [ ] Different API endpoints that use different pagination param names have separate subBlocks in the block
- [ ] Pagination response fields (`nextToken`, `cursor`, etc.) are included in tool outputs
- [ ] Pagination subBlocks are set to `mode: 'advanced'`
## Step 7: Validate Error Handling
- [ ] `transformResponse` checks for error conditions before accessing data
- [ ] Error responses include meaningful messages (not just generic "failed")
- [ ] HTTP error status codes are handled (check `response.ok` or status codes)
## Step 8: Report and Fix
### Report Format
Group findings by severity:
**Critical** (will cause runtime errors or incorrect behavior):
- Wrong endpoint URL or HTTP method
- Missing required params or wrong `required` flag
- Incorrect response field mapping (accessing wrong path in response)
- Missing error handling that would cause crashes
- Tool ID mismatch between tool file, registry, and block `tools.access`
- OAuth scopes missing in `auth.ts` that tools need
- `tools.config.tool` returning wrong tool ID for an operation
- Type coercions in `tools.config.tool` instead of `tools.config.params`
**Warning** (follows conventions incorrectly or has usability issues):
- Optional field not set to `mode: 'advanced'`
- Missing `wandConfig` on timestamp/complex fields
- Wrong `visibility` on params (e.g., `'hidden'` instead of `'user-or-llm'`)
- Missing `optional: true` on nullable outputs
- Opaque `type: 'json'` without property descriptions
- Missing `.trim()` on ID fields in request URLs
- Missing `?? null` on nullable response fields
- Block condition array missing an operation that uses that field
- Hardcoded scope arrays instead of using `getScopesForService()` / `getCanonicalScopesForProvider()`
- Missing scope description in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`
**Suggestion** (minor improvements):
- Better description text
- Inconsistent naming across tools
- Missing `longDescription` or `docsLink`
- Pagination fields that could benefit from `wandConfig`
### Fix All Issues
After reporting, fix every **critical** and **warning** issue. Apply **suggestions** where they don't add unnecessary complexity.
### Validation Output
After fixing, confirm:
1. `bun run lint` passes with no fixes needed
2. TypeScript compiles clean (no type errors)
3. Re-read all modified files to verify fixes are correct
## Checklist Summary
- [ ] Read ALL tool files, block, types, index, and registries
- [ ] Pulled and read official API documentation
- [ ] Validated every tool's ID, params, request, response, outputs, and types against API docs
- [ ] Validated block ↔ tool alignment (every tool param has a subBlock, every condition is correct)
- [ ] Validated advanced mode on optional/rarely-used fields
- [ ] Validated wandConfig on timestamps and complex inputs
- [ ] Validated tools.config mapping, tool selector, and type coercions
- [ ] Validated block outputs match what tools return, with typed JSON where possible
- [ ] Validated OAuth scopes use centralized utilities (getScopesForService, getCanonicalScopesForProvider) — no hardcoded arrays
- [ ] Validated scope descriptions exist in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts` for all scopes
- [ ] Validated pagination consistency across tools and block
- [ ] Validated error handling (error checks, meaningful messages)
- [ ] Validated registry entries (tools and block, alphabetical, correct imports)
- [ ] Reported all issues grouped by severity
- [ ] Fixed all critical and warning issues
- [ ] Ran `bun run lint` after fixes
- [ ] Verified TypeScript compiles clean

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Validate Integration"
short_description: "Audit a Sim service integration"
brand_color: "#B45309"
default_prompt: "Use $validate-integration to audit and fix a Sim integration against its API docs."

View File

@@ -19,7 +19,7 @@ When the user asks you to create a block:
```typescript
import { {ServiceName}Icon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { getScopesForService } from '@/lib/oauth/utils'
export const {ServiceName}Block: BlockConfig = {
@@ -29,6 +29,8 @@ export const {ServiceName}Block: BlockConfig = {
longDescription: 'Detailed description for docs',
docsLink: 'https://docs.sim.ai/tools/{service}',
category: 'tools', // 'tools' | 'blocks' | 'triggers'
integrationType: IntegrationType.X, // Primary category (see IntegrationType enum)
tags: ['oauth', 'api'], // Cross-cutting tags (see IntegrationTag type)
bgColor: '#HEXCOLOR', // Brand color
icon: {ServiceName}Icon,
@@ -629,7 +631,7 @@ export const registry: Record<string, BlockConfig> = {
```typescript
import { ServiceIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { getScopesForService } from '@/lib/oauth/utils'
export const ServiceBlock: BlockConfig = {
@@ -639,6 +641,8 @@ export const ServiceBlock: BlockConfig = {
longDescription: 'Full description for documentation...',
docsLink: 'https://docs.sim.ai/tools/service',
category: 'tools',
integrationType: IntegrationType.DeveloperTools,
tags: ['oauth', 'api'],
bgColor: '#FF6B6B',
icon: ServiceIcon,
authMode: AuthMode.OAuth,
@@ -796,6 +800,8 @@ All tool IDs referenced in `tools.access` and returned by `tools.config.tool` MU
## Checklist Before Finishing
- [ ] `integrationType` is set to the correct `IntegrationType` enum value
- [ ] `tags` array includes all applicable `IntegrationTag` values
- [ ] All subBlocks have `id`, `title` (except switch), and `type`
- [ ] Conditions use correct syntax (field, value, not, and)
- [ ] DependsOn set for fields that need other values

View File

@@ -113,7 +113,7 @@ export const {service}{Action}Tool: ToolConfig<Params, Response> = {
```typescript
import { {Service}Icon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { getScopesForService } from '@/lib/oauth/utils'
export const {Service}Block: BlockConfig = {
@@ -123,6 +123,8 @@ export const {Service}Block: BlockConfig = {
longDescription: '...',
docsLink: 'https://docs.sim.ai/tools/{service}',
category: 'tools',
integrationType: IntegrationType.X, // Primary category (see IntegrationType enum)
tags: ['oauth', 'api'], // Cross-cutting tags (see IntegrationTag type)
bgColor: '#HEXCOLOR',
icon: {Service}Icon,
authMode: AuthMode.OAuth, // or AuthMode.ApiKey
@@ -410,6 +412,8 @@ If creating V2 versions (API-aligned outputs):
### Block
- [ ] Created `blocks/blocks/{service}.ts`
- [ ] Set `integrationType` to the correct `IntegrationType` enum value
- [ ] Set `tags` array with all applicable `IntegrationTag` values
- [ ] Defined operation dropdown with all operations
- [ ] Added credential field with `requiredScopes: getScopesForService('{service}')`
- [ ] Added conditional fields per operation

View File

@@ -192,7 +192,7 @@ In the block config (`blocks/blocks/{service}.ts`), add `hideWhenHosted: true` t
},
```
The visibility is controlled by `isSubBlockHiddenByHostedKey()` in `lib/workflows/subblocks/visibility.ts`, which checks the `isHosted` feature flag.
The visibility is controlled by `isSubBlockHidden()` in `lib/workflows/subblocks/visibility.ts`, which checks both the `isHosted` feature flag (`hideWhenHosted`) and optional env var conditions (`hideWhenEnvSet`).
### Excluding Specific Operations from Hosted Key Support

View File

@@ -1,4 +1,4 @@
FROM oven/bun:1.3.10-alpine
FROM oven/bun:1.3.11-alpine
# Install necessary packages for development
RUN apk add --no-cache \

View File

@@ -2,9 +2,9 @@ name: CI
on:
push:
branches: [main, staging]
branches: [main, staging, dev]
pull_request:
branches: [main, staging]
branches: [main, staging, dev]
concurrency:
group: ci-${{ github.ref }}
@@ -23,7 +23,7 @@ jobs:
detect-version:
name: Detect Version
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging' || github.ref == 'refs/heads/dev')
outputs:
version: ${{ steps.extract.outputs.version }}
is_release: ${{ steps.extract.outputs.is_release }}
@@ -49,7 +49,7 @@ jobs:
build-amd64:
name: Build AMD64
needs: [test-build, detect-version]
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging' || github.ref == 'refs/heads/dev')
runs-on: blacksmith-8vcpu-ubuntu-2404
permissions:
contents: read
@@ -75,8 +75,8 @@ jobs:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || secrets.STAGING_AWS_REGION }}
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || github.ref == 'refs/heads/dev' && secrets.DEV_AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || github.ref == 'refs/heads/dev' && secrets.DEV_AWS_REGION || secrets.STAGING_AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
@@ -109,6 +109,8 @@ jobs:
# ECR tags (always build for ECR)
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
ECR_TAG="latest"
elif [ "${{ github.ref }}" = "refs/heads/dev" ]; then
ECR_TAG="dev"
else
ECR_TAG="staging"
fi

View File

@@ -20,7 +20,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
bun-version: 1.3.11
- name: Setup Node
uses: actions/setup-node@v4

View File

@@ -23,7 +23,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
bun-version: 1.3.11
- name: Cache Bun dependencies
uses: actions/cache@v4
@@ -122,7 +122,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
bun-version: 1.3.11
- name: Cache Bun dependencies
uses: actions/cache@v4

View File

@@ -36,8 +36,8 @@ jobs:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || secrets.STAGING_AWS_REGION }}
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || github.ref == 'refs/heads/dev' && secrets.DEV_AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || github.ref == 'refs/heads/dev' && secrets.DEV_AWS_REGION || secrets.STAGING_AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
@@ -70,6 +70,8 @@ jobs:
# ECR tags (always build for ECR)
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
ECR_TAG="latest"
elif [ "${{ github.ref }}" = "refs/heads/dev" ]; then
ECR_TAG="dev"
else
ECR_TAG="staging"
fi

View File

@@ -19,7 +19,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
bun-version: 1.3.11
- name: Cache Bun dependencies
uses: actions/cache@v4

View File

@@ -19,7 +19,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
bun-version: 1.3.11
- name: Setup Node.js for npm publishing
uses: actions/setup-node@v4

View File

@@ -19,7 +19,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
bun-version: 1.3.11
- name: Setup Node.js for npm publishing
uses: actions/setup-node@v4

View File

@@ -19,7 +19,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
bun-version: 1.3.11
- name: Setup Node
uses: actions/setup-node@v4

383
AGENTS.md Normal file
View File

@@ -0,0 +1,383 @@
# Sim Development Guidelines
You are a professional software engineer. All code must follow best practices: accurate, readable, clean, and efficient.
## Global Standards
- **Logging**: Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`
- **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments
- **Styling**: Never update global styles. Keep all styling local to components
- **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx`
## Architecture
### Core Principles
1. Single Responsibility: Each component, hook, store has one clear purpose
2. Composition Over Complexity: Break down complex logic into smaller pieces
3. Type Safety First: TypeScript interfaces for all props, state, return types
4. Predictable State: Zustand for global state, useState for UI-only concerns
### 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
```
### Naming Conventions
- Components: PascalCase (`WorkflowList`)
- Hooks: `use` prefix (`useWorkflowOperations`)
- Files: kebab-case (`workflow-list.tsx`)
- Stores: `stores/feature/store.ts`
- Constants: SCREAMING_SNAKE_CASE
- Interfaces: PascalCase with suffix (`WorkflowListProps`)
## Imports
**Always use absolute imports.** Never use relative imports.
```typescript
// ✓ Good
import { useWorkflowStore } from '@/stores/workflows/store'
// ✗ Bad
import { useWorkflowStore } from '../../../stores/workflows/store'
```
Use barrel exports (`index.ts`) when a folder has 3+ exports. Do not re-export from non-barrel files; import directly from the source.
### Import Order
1. React/core libraries
2. External libraries
3. UI components (`@/components/emcn`, `@/components/ui`)
4. Utilities (`@/lib/...`)
5. Stores (`@/stores/...`)
6. Feature imports
7. CSS imports
Use `import type { X }` for type-only imports.
## TypeScript
1. No `any` - Use proper types or `unknown` with type guards
2. Always define props interface for components
3. `as const` for constant objects/arrays
4. Explicit ref types: `useRef<HTMLDivElement>(null)`
## Components
```typescript
'use client' // Only if using hooks
const CONFIG = { SPACING: 8 } as const
interface ComponentProps {
requiredProp: string
optionalProp?: boolean
}
export function Component({ requiredProp, optionalProp = false }: ComponentProps) {
// Order: refs → external hooks → store hooks → custom hooks → state → useMemo → useCallback → useEffect → return
}
```
Extract when: 50+ lines, used in 2+ files, or has own state/logic. Keep inline when: < 10 lines, single use, purely presentational.
## Hooks
```typescript
interface UseFeatureProps { id: string }
export function useFeature({ id }: UseFeatureProps) {
const idRef = useRef(id)
const [data, setData] = useState<Data | null>(null)
useEffect(() => { idRef.current = id }, [id])
const fetchData = useCallback(async () => { ... }, []) // Empty deps when using refs
return { data, fetchData }
}
```
## Zustand Stores
Stores live in `stores/`. Complex stores split into `store.ts` + `types.ts`.
```typescript
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
const initialState = { items: [] as Item[] }
export const useFeatureStore = create<FeatureState>()(
devtools(
(set, get) => ({
...initialState,
setItems: (items) => set({ items }),
reset: () => set(initialState),
}),
{ name: 'feature-store' }
)
)
```
Use `devtools` middleware. Use `persist` only when data should survive reload with `partialize` to persist only necessary state.
## React Query
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.
### Query Key Factory
Every file must have a hierarchical key factory with an `all` root key and intermediate plural keys for prefix invalidation:
```typescript
export const entityKeys = {
all: ['entity'] as const,
lists: () => [...entityKeys.all, 'list'] as const,
list: (workspaceId?: string) => [...entityKeys.lists(), workspaceId ?? ''] as const,
details: () => [...entityKeys.all, 'detail'] as const,
detail: (id?: string) => [...entityKeys.details(), id ?? ''] as const,
}
```
### Query Hooks
- Every `queryFn` must 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
```typescript
export function useEntityList(workspaceId?: string) {
return useQuery({
queryKey: entityKeys.list(workspaceId),
queryFn: ({ signal }) => fetchEntities(workspaceId as string, signal),
enabled: Boolean(workspaceId),
staleTime: 60 * 1000,
placeholderData: keepPreviousData, // OK: workspaceId varies
})
}
```
### Mutation Hooks
- Use targeted invalidation (`entityKeys.lists()`) not broad (`entityKeys.all`) when possible
- For optimistic updates: use `onSettled` (not `onSuccess`) for cache reconciliation — `onSettled` fires on both success and error
- Don't include mutation objects in `useCallback` deps — `.mutate()` is stable in TanStack Query v5
```typescript
export function useUpdateEntity() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (variables) => { /* ... */ },
onMutate: async (variables) => {
await queryClient.cancelQueries({ queryKey: entityKeys.detail(variables.id) })
const previous = queryClient.getQueryData(entityKeys.detail(variables.id))
queryClient.setQueryData(entityKeys.detail(variables.id), /* optimistic */)
return { previous }
},
onError: (_err, variables, context) => {
queryClient.setQueryData(entityKeys.detail(variables.id), context?.previous)
},
onSettled: (_data, _error, variables) => {
queryClient.invalidateQueries({ queryKey: entityKeys.lists() })
queryClient.invalidateQueries({ queryKey: entityKeys.detail(variables.id) })
},
})
}
```
## Styling
Use Tailwind only, no inline styles. Use `cn()` from `@/lib/utils` for conditional classes.
```typescript
<div className={cn('base-classes', isActive && 'active-classes')} />
```
## EMCN Components
Import from `@/components/emcn`, never from subpaths (except CSS files). Use CVA when 2+ variants exist.
## Testing
Use Vitest. Test files: `feature.ts``feature.test.ts`. See `.cursor/rules/sim-testing.mdc` for full details.
### 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.
### Standard Test Pattern
```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 } from '@/app/api/my-route/route'
describe('my route', () => {
beforeEach(() => {
vi.clearAllMocks()
mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
})
it('returns data', async () => { ... })
})
```
### Performance Rules
- **NEVER** use `vi.resetModules()` + `vi.doMock()` + `await import()` — use `vi.hoisted()` + `vi.mock()` + static imports
- **NEVER** use `vi.importActual()` — mock everything explicitly
- **NEVER** use `mockAuth()`, `mockConsoleLogger()`, `setupCommonApiMocks()` from `@sim/testing` — they use `vi.doMock()` internally
- **Mock heavy deps** (`@/blocks`, `@/tools/registry`, `@/triggers`) in tests that don't need them
- **Use `@vitest-environment node`** unless DOM APIs are needed (`window`, `document`, `FormData`)
- **Avoid real timers** — use 1ms delays or `vi.useFakeTimers()`
Use `@sim/testing` mocks/factories over local test data.
## Utils Rules
- Never create `utils.ts` for single consumer - inline it
- Create `utils.ts` when 2+ files need the same helper
- Check existing sources in `lib/` before duplicating
## Adding Integrations
New integrations require: **Tools****Block****Icon** → (optional) **Trigger**
Always look up the service's API docs first.
### 1. Tools (`tools/{service}/`)
```
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

View File

@@ -1,16 +1,20 @@
<p align="center">
<a href="https://sim.ai" target="_blank" rel="noopener noreferrer">
<img src="apps/sim/public/logo/reverse/text/large.png" alt="Sim Logo" width="500"/>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="apps/sim/public/logo/wordmark.svg">
<source media="(prefers-color-scheme: light)" srcset="apps/sim/public/logo/wordmark-dark.svg">
<img src="apps/sim/public/logo/wordmark-dark.svg" alt="Sim Logo" width="380"/>
</picture>
</a>
</p>
<p align="center">The open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to orchestrate agentic workflows.</p>
<p align="center">
<a href="https://sim.ai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/sim.ai-6F3DFA" alt="Sim.ai"></a>
<a href="https://sim.ai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/sim.ai-33c482" alt="Sim.ai"></a>
<a href="https://discord.gg/Hr4UWYEcTT" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Discord-Join%20Server-5865F2?logo=discord&logoColor=white" alt="Discord"></a>
<a href="https://x.com/simdotai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/twitter/follow/simdotai?style=social" alt="Twitter"></a>
<a href="https://docs.sim.ai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Docs-6F3DFA.svg" alt="Documentation"></a>
<a href="https://docs.sim.ai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Docs-33c482.svg" alt="Documentation"></a>
</p>
<p align="center">
@@ -42,7 +46,7 @@ Upload documents to a vector store and let agents answer questions grounded in y
### Cloud-hosted: [sim.ai](https://sim.ai)
<a href="https://sim.ai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/sim.ai-6F3DFA?logo=data:image/svg%2bxml;base64,PHN2ZyB3aWR0aD0iNjE2IiBoZWlnaHQ9IjYxNiIgdmlld0JveD0iMCAwIDYxNiA2MTYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8xMTU5XzMxMykiPgo8cGF0aCBkPSJNNjE2IDBIMFY2MTZINjE2VjBaIiBmaWxsPSIjNkYzREZBIi8+CjxwYXRoIGQ9Ik04MyAzNjUuNTY3SDExM0MxMTMgMzczLjgwNSAxMTYgMzgwLjM3MyAxMjIgMzg1LjI3MkMxMjggMzg5Ljk0OCAxMzYuMTExIDM5Mi4yODUgMTQ2LjMzMyAzOTIuMjg1QzE1Ny40NDQgMzkyLjI4NSAxNjYgMzkwLjE3MSAxNzIgMzg1LjkzOUMxNzcuOTk5IDM4MS40ODcgMTgxIDM3NS41ODYgMTgxIDM2OC4yMzlDMTgxIDM2Mi44OTUgMTc5LjMzMyAzNTguNDQyIDE3NiAzNTQuODhDMTcyLjg4OSAzNTEuMzE4IDE2Ny4xMTEgMzQ4LjQyMiAxNTguNjY3IDM0Ni4xOTZMMTMwIDMzOS41MTdDMTE1LjU1NSAzMzUuOTU1IDEwNC43NzggMzMwLjQ5OSA5Ny42NjY1IDMyMy4xNTFDOTAuNzc3NSAzMTUuODA0IDg3LjMzMzQgMzA2LjExOSA4Ny4zMzM0IDI5NC4wOTZDODcuMzMzNCAyODQuMDc2IDg5Ljg4OSAyNzUuMzkyIDk0Ljk5OTYgMjY4LjA0NUMxMDAuMzMzIDI2MC42OTcgMTA3LjU1NSAyNTUuMDIgMTE2LjY2NiAyNTEuMDEyQzEyNiAyNDcuMDA0IDEzNi42NjcgMjQ1IDE0OC42NjYgMjQ1QzE2MC42NjcgMjQ1IDE3MSAyNDcuMTE2IDE3OS42NjcgMjUxLjM0NkMxODguNTU1IDI1NS41NzYgMTk1LjQ0NCAyNjEuNDc3IDIwMC4zMzMgMjY5LjA0N0MyMDUuNDQ0IDI3Ni42MTcgMjA4LjExMSAyODUuNjM0IDIwOC4zMzMgMjk2LjA5OUgxNzguMzMzQzE3OC4xMTEgMjg3LjYzOCAxNzUuMzMzIDI4MS4wNyAxNjkuOTk5IDI3Ni4zOTRDMTY0LjY2NiAyNzEuNzE5IDE1Ny4yMjIgMjY5LjM4MSAxNDcuNjY3IDI2OS4zODFDMTM3Ljg4OSAyNjkuMzgxIDEzMC4zMzMgMjcxLjQ5NiAxMjUgMjc1LjcyNkMxMTkuNjY2IDI3OS45NTcgMTE3IDI4NS43NDYgMTE3IDI5My4wOTNDMTE3IDMwNC4wMDMgMTI1IDMxMS40NjIgMTQxIDMxNS40N0wxNjkuNjY3IDMyMi40ODNDMTgzLjQ0NSAzMjUuNiAxOTMuNzc4IDMzMC43MjIgMjAwLjY2NyAzMzcuODQ3QzIwNy41NTUgMzQ0Ljc0OSAyMTEgMzU0LjIxMiAyMTEgMzY2LjIzNUMyMTEgMzc2LjQ3NyAyMDguMjIyIDM4NS40OTQgMjAyLjY2NiAzOTMuMjg3QzE5Ny4xMTEgNDAwLjg1NyAxODkuNDQ0IDQwNi43NTggMTc5LjY2NyA0MTAuOTg5QzE3MC4xMTEgNDE0Ljk5NiAxNTguNzc4IDQxNyAxNDUuNjY3IDQxN0MxMjYuNTU1IDQxNyAxMTEuMzMzIDQxMi4zMjUgOTkuOTk5NyA0MDIuOTczQzg4LjY2NjggMzkzLjYyMSA4MyAzODEuMTUzIDgzIDM2NS41NjdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMjMyLjI5MSA0MTNWMjUwLjA4MkMyNDQuNjg0IDI1NC42MTQgMjUwLjE0OCAyNTQuNjE0IDI2My4zNzEgMjUwLjA4MlY0MTNIMjMyLjI5MVpNMjQ3LjUgMjM5LjMxM0MyNDEuOTkgMjM5LjMxMyAyMzcuMTQgMjM3LjMxMyAyMzIuOTUyIDIzMy4zMTZDMjI4Ljk4NCAyMjkuMDk1IDIyNyAyMjQuMjA5IDIyNyAyMTguNjU2QzIyNyAyMTIuODgyIDIyOC45ODQgMjA3Ljk5NSAyMzIuOTUyIDIwMy45OTdDMjM3LjE0IDE5OS45OTkgMjQxLjk5IDE5OCAyNDcuNSAxOThDMjUzLjIzMSAxOTggMjU4LjA4IDE5OS45OTkgMjYyLjA0OSAyMDMuOTk3QzI2Ni4wMTYgMjA3Ljk5NSAyNjggMjEyLjg4MiAyNjggMjE4LjY1NkMyNjggMjI0LjIwOSAyNjYuMDE2IDIyOS4wOTUgMjYyLjA0OSAyMzMuMzE2QzI1OC4wOCAyMzcuMzEzIDI1My4yMzEgMjM5LjMxMyAyNDcuNSAyMzkuMzEzWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTMxOS4zMzMgNDEzSDI4OFYyNDkuNjc2SDMxNlYyNzcuMjMzQzMxOS4zMzMgMjY4LjEwNCAzMjUuNzc4IDI2MC4zNjQgMzM0LjY2NyAyNTQuMzUyQzM0My43NzggMjQ4LjExNyAzNTQuNzc4IDI0NSAzNjcuNjY3IDI0NUMzODIuMTExIDI0NSAzOTQuMTEyIDI0OC44OTcgNDAzLjY2NyAyNTYuNjlDNDEzLjIyMiAyNjQuNDg0IDQxOS40NDQgMjc0LjgzNyA0MjIuMzM0IDI4Ny43NTJINDE2LjY2N0M0MTguODg5IDI3NC44MzcgNDI1IDI2NC40ODQgNDM1IDI1Ni42OUM0NDUgMjQ4Ljg5NyA0NTcuMzM0IDI0NSA0NzIgMjQ1QzQ5MC42NjYgMjQ1IDUwNS4zMzQgMjUwLjQ1NSA1MTYgMjYxLjM2NkM1MjYuNjY3IDI3Mi4yNzYgNTMyIDI4Ny4xOTUgNTMyIDMwNi4xMjFWNDEzSDUwMS4zMzNWMzEzLjgwNEM1MDEuMzMzIDMwMC44ODkgNDk4IDI5MC45ODEgNDkxLjMzMyAyODQuMDc4QzQ4NC44ODkgMjc2Ljk1MiA0NzYuMTExIDI3My4zOSA0NjUgMjczLjM5QzQ1Ny4yMjIgMjczLjM5IDQ1MC4zMzMgMjc1LjE3MSA0NDQuMzM0IDI3OC43MzRDNDM4LjU1NiAyODIuMDc0IDQzNCAyODYuOTcyIDQzMC42NjcgMjkzLjQzQzQyNy4zMzMgMjk5Ljg4NyA0MjUuNjY3IDMwNy40NTcgNDI1LjY2NyAzMTYuMTQxVjQxM0gzOTQuNjY3VjMxMy40NjlDMzk0LjY2NyAzMDAuNTU1IDM5MS40NDUgMjkwLjc1OCAzODUgMjg0LjA3OEMzNzguNTU2IDI3Ny4xNzUgMzY5Ljc3OCAyNzMuNzI0IDM1OC42NjcgMjczLjcyNEMzNTAuODg5IDI3My43MjQgMzQ0IDI3NS41MDUgMzM4IDI3OS4wNjhDMzMyLjIyMiAyODIuNDA4IDMyNy42NjcgMjg3LjMwNyAzMjQuMzMzIDI5My43NjNDMzIxIDI5OS45OTggMzE5LjMzMyAzMDcuNDU3IDMxOS4zMzMgMzE2LjE0MVY0MTNaIiBmaWxsPSJ3aGl0ZSIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzExNTlfMzEzIj4KPHJlY3Qgd2lkdGg9IjYxNiIgaGVpZ2h0PSI2MTYiIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg==&logoColor=white" alt="Sim.ai"></a>
<a href="https://sim.ai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/sim.ai-33c482?logo=data:image/svg%2bxml;base64,PHN2ZyB3aWR0aD0iNjE2IiBoZWlnaHQ9IjYxNiIgdmlld0JveD0iMCAwIDYxNiA2MTYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8xMTU5XzMxMykiPgo8cGF0aCBkPSJNNjE2IDBIMFY2MTZINjE2VjBaIiBmaWxsPSIjMzNjNDgyIi8+CjxwYXRoIGQ9Ik04MyAzNjUuNTY3SDExM0MxMTMgMzczLjgwNSAxMTYgMzgwLjM3MyAxMjIgMzg1LjI3MkMxMjggMzg5Ljk0OCAxMzYuMTExIDM5Mi4yODUgMTQ2LjMzMyAzOTIuMjg1QzE1Ny40NDQgMzkyLjI4NSAxNjYgMzkwLjE3MSAxNzIgMzg1LjkzOUMxNzcuOTk5IDM4MS40ODcgMTgxIDM3NS41ODYgMTgxIDM2OC4yMzlDMTgxIDM2Mi44OTUgMTc5LjMzMyAzNTguNDQyIDE3NiAzNTQuODhDMTcyLjg4OSAzNTEuMzE4IDE2Ny4xMTEgMzQ4LjQyMiAxNTguNjY3IDM0Ni4xOTZMMTMwIDMzOS41MTdDMTE1LjU1NSAzMzUuOTU1IDEwNC43NzggMzMwLjQ5OSA5Ny42NjY1IDMyMy4xNTFDOTAuNzc3NSAzMTUuODA0IDg3LjMzMzQgMzA2LjExOSA4Ny4zMzM0IDI5NC4wOTZDODcuMzMzNCAyODQuMDc2IDg5Ljg4OSAyNzUuMzkyIDk0Ljk5OTYgMjY4LjA0NUMxMDAuMzMzIDI2MC42OTcgMTA3LjU1NSAyNTUuMDIgMTE2LjY2NiAyNTEuMDEyQzEyNiAyNDcuMDA0IDEzNi42NjcgMjQ1IDE0OC42NjYgMjQ1QzE2MC42NjcgMjQ1IDE3MSAyNDcuMTE2IDE3OS42NjcgMjUxLjM0NkMxODguNTU1IDI1NS41NzYgMTk1LjQ0NCAyNjEuNDc3IDIwMC4zMzMgMjY5LjA0N0MyMDUuNDQ0IDI3Ni42MTcgMjA4LjExMSAyODUuNjM0IDIwOC4zMzMgMjk2LjA5OUgxNzguMzMzQzE3OC4xMTEgMjg3LjYzOCAxNzUuMzMzIDI4MS4wNyAxNjkuOTk5IDI3Ni4zOTRDMTY0LjY2NiAyNzEuNzE5IDE1Ny4yMjIgMjY5LjM4MSAxNDcuNjY3IDI2OS4zODFDMTM3Ljg4OSAyNjkuMzgxIDEzMC4zMzMgMjcxLjQ5NiAxMjUgMjc1LjcyNkMxMTkuNjY2IDI3OS45NTcgMTE3IDI4NS43NDYgMTE3IDI5My4wOTNDMTE3IDMwNC4wMDMgMTI1IDMxMS40NjIgMTQxIDMxNS40N0wxNjkuNjY3IDMyMi40ODNDMTgzLjQ0NSAzMjUuNiAxOTMuNzc4IDMzMC43MjIgMjAwLjY2NyAzMzcuODQ3QzIwNy41NTUgMzQ0Ljc0OSAyMTEgMzU0LjIxMiAyMTEgMzY2LjIzNUMyMTEgMzc2LjQ3NyAyMDguMjIyIDM4NS40OTQgMjAyLjY2NiAzOTMuMjg3QzE5Ny4xMTEgNDAwLjg1NyAxODkuNDQ0IDQwNi43NTggMTc5LjY2NyA0MTAuOTg5QzE3MC4xMTEgNDE0Ljk5NiAxNTguNzc4IDQxNyAxNDUuNjY3IDQxN0MxMjYuNTU1IDQxNyAxMTEuMzMzIDQxMi4zMjUgOTkuOTk5NyA0MDIuOTczQzg4LjY2NjggMzkzLjYyMSA4MyAzODEuMTUzIDgzIDM2NS41NjdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMjMyLjI5MSA0MTNWMjUwLjA4MkMyNDQuNjg0IDI1NC42MTQgMjUwLjE0OCAyNTQuNjE0IDI2My4zNzEgMjUwLjA4MlY0MTNIMjMyLjI5MVpNMjQ3LjUgMjM5LjMxM0MyNDEuOTkgMjM5LjMxMyAyMzcuMTQgMjM3LjMxMyAyMzIuOTUyIDIzMy4zMTZDMjI4Ljk4NCAyMjkuMDk1IDIyNyAyMjQuMjA5IDIyNyAyMTguNjU2QzIyNyAyMTIuODgyIDIyOC45ODQgMjA3Ljk5NSAyMzIuOTUyIDIwMy45OTdDMjM3LjE0IDE5OS45OTkgMjQxLjk5IDE5OCAyNDcuNSAxOThDMjUzLjIzMSAxOTggMjU4LjA4IDE5OS45OTkgMjYyLjA0OSAyMDMuOTk3QzI2Ni4wMTYgMjA3Ljk5NSAyNjggMjEyLjg4MiAyNjggMjE4LjY1NkMyNjggMjI0LjIwOSAyNjYuMDE2IDIyOS4wOTUgMjYyLjA0OSAyMzMuMzE2QzI1OC4wOCAyMzcuMzEzIDI1My4yMzEgMjM5LjMxMyAyNDcuNSAyMzkuMzEzWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTMxOS4zMzMgNDEzSDI4OFYyNDkuNjc2SDMxNlYyNzcuMjMzQzMxOS4zMzMgMjY4LjEwNCAzMjUuNzc4IDI2MC4zNjQgMzM0LjY2NyAyNTQuMzUyQzM0My43NzggMjQ4LjExNyAzNTQuNzc4IDI0NSAzNjcuNjY3IDI0NUMzODIuMTExIDI0NSAzOTQuMTEyIDI0OC44OTcgNDAzLjY2NyAyNTYuNjlDNDEzLjIyMiAyNjQuNDg0IDQxOS40NDQgMjc0LjgzNyA0MjIuMzM0IDI4Ny43NTJINDE2LjY2N0M0MTguODg5IDI3NC44MzcgNDI1IDI2NC40ODQgNDM1IDI1Ni42OUM0NDUgMjQ4Ljg5NyA0NTcuMzM0IDI0NSA0NzIgMjQ1QzQ5MC42NjYgMjQ1IDUwNS4zMzQgMjUwLjQ1NSA1MTYgMjYxLjM2NkM1MjYuNjY3IDI3Mi4yNzYgNTMyIDI4Ny4xOTUgNTMyIDMwNi4xMjFWNDEzSDUwMS4zMzNWMzEzLjgwNEM1MDEuMzMzIDMwMC44ODkgNDk4IDI5MC45ODEgNDkxLjMzMyAyODQuMDc4QzQ4NC44ODkgMjc2Ljk1MiA0NzYuMTExIDI3My4zOSA0NjUgMjczLjM5QzQ1Ny4yMjIgMjczLjM5IDQ1MC4zMzMgMjc1LjE3MSA0NDQuMzM0IDI3OC43MzRDNDM4LjU1NiAyODIuMDc0IDQzNCAyODYuOTcyIDQzMC42NjcgMjkzLjQzQzQyNy4zMzMgMjk5Ljg4NyA0MjUuNjY3IDMwNy40NTcgNDI1LjY2NyAzMTYuMTQxVjQxM0gzOTQuNjY3VjMxMy40NjlDMzk0LjY2NyAzMDAuNTU1IDM5MS40NDUgMjkwLjc1OCAzODUgMjg0LjA3OEMzNzguNTU2IDI3Ny4xNzUgMzY5Ljc3OCAyNzMuNzI0IDM1OC42NjcgMjczLjcyNEMzNTAuODg5IDI3My43MjQgMzQ0IDI3NS41MDUgMzM4IDI3OS4wNjhDMzMyLjIyMiAyODIuNDA4IDMyNy42NjcgMjg3LjMwNyAzMjQuMzMzIDI5My43NjNDMzIxIDI5OS45OTggMzE5LjMzMyAzMDcuNDU3IDMxOS4zMzMgMzE2LjE0MVY0MTNaIiBmaWxsPSJ3aGl0ZSIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzExNTlfMzEzIj4KPHJlY3Qgd2lkdGg9IjYxNiIgaGVpZ2h0PSI2MTYiIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+&logoColor=white" alt="Sim.ai"></a>
### Self-hosted: NPM Package
@@ -70,43 +74,11 @@ docker compose -f docker-compose.prod.yml up -d
Open [http://localhost:3000](http://localhost:3000)
#### Using Local Models with Ollama
#### Background worker note
Run Sim with local AI models using [Ollama](https://ollama.ai) - no external APIs required:
The Docker Compose stack starts a dedicated worker container by default. If `REDIS_URL` is not configured, the worker will start, log that it is idle, and do no queue processing. This is expected. Queue-backed API, webhook, and schedule execution requires Redis; installs without Redis continue to use the inline execution path.
```bash
# Start with GPU support (automatically downloads gemma3:4b model)
docker compose -f docker-compose.ollama.yml --profile setup up -d
# For CPU-only systems:
docker compose -f docker-compose.ollama.yml --profile cpu --profile setup up -d
```
Wait for the model to download, then visit [http://localhost:3000](http://localhost:3000). Add more models with:
```bash
docker compose -f docker-compose.ollama.yml exec ollama ollama pull llama3.1:8b
```
#### Using an External Ollama Instance
If Ollama is running on your host machine, use `host.docker.internal` instead of `localhost`:
```bash
OLLAMA_URL=http://host.docker.internal:11434 docker compose -f docker-compose.prod.yml up -d
```
On Linux, use your host's IP address or add `extra_hosts: ["host.docker.internal:host-gateway"]` to the compose file.
#### Using vLLM
Sim supports [vLLM](https://docs.vllm.ai/) for self-hosted models. Set `VLLM_BASE_URL` and optionally `VLLM_API_KEY` in your environment.
### Self-hosted: Dev Containers
1. Open VS Code with the [Remote - Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
2. Open the project and click "Reopen in Container" when prompted
3. Run `bun run dev:full` in the terminal or use the `sim-start` alias
- This starts both the main application and the realtime socket server
Sim also supports local models via [Ollama](https://ollama.ai) and [vLLM](https://docs.vllm.ai/) — see the [Docker self-hosting docs](https://docs.sim.ai/self-hosting/docker) for setup details.
### Self-hosted: Manual Setup
@@ -145,10 +117,12 @@ cd packages/db && bunx drizzle-kit migrate --config=./drizzle.config.ts
5. Start development servers:
```bash
bun run dev:full # Starts both Next.js app and realtime socket server
bun run dev:full # Starts Next.js app, realtime socket server, and the BullMQ worker
```
Or run separately: `bun run dev` (Next.js) and `cd apps/sim && bun run dev:sockets` (realtime).
If `REDIS_URL` is not configured, the worker will remain idle and execution continues inline.
Or run separately: `bun run dev` (Next.js), `cd apps/sim && bun run dev:sockets` (realtime), and `cd apps/sim && bun run worker` (BullMQ worker).
## Copilot API Keys
@@ -159,18 +133,7 @@ Copilot is a Sim-managed service. To use Copilot on a self-hosted instance:
## Environment Variables
Key environment variables for self-hosted deployments. See [`.env.example`](apps/sim/.env.example) for defaults or [`env.ts`](apps/sim/lib/core/config/env.ts) for the full list.
| Variable | Required | Description |
|----------|----------|-------------|
| `DATABASE_URL` | Yes | PostgreSQL connection string with pgvector |
| `BETTER_AUTH_SECRET` | Yes | Auth secret (`openssl rand -hex 32`) |
| `BETTER_AUTH_URL` | Yes | Your app URL (e.g., `http://localhost:3000`) |
| `NEXT_PUBLIC_APP_URL` | Yes | Public app URL (same as above) |
| `ENCRYPTION_KEY` | Yes | Encrypts environment variables (`openssl rand -hex 32`) |
| `INTERNAL_API_SECRET` | Yes | Encrypts internal API routes (`openssl rand -hex 32`) |
| `API_ENCRYPTION_KEY` | Yes | Encrypts API keys (`openssl rand -hex 32`) |
| `COPILOT_API_KEY` | No | API key from sim.ai for Copilot features |
See the [environment variables reference](https://docs.sim.ai/self-hosting/environment-variables) for the full list, or [`apps/sim/.env.example`](apps/sim/.env.example) for defaults.
## Tech Stack

View File

@@ -8,8 +8,6 @@ export default function RootLayout({ children }: { children: ReactNode }) {
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
userScalable: false,
themeColor: [
{ media: '(prefers-color-scheme: light)', color: '#ffffff' },
{ media: '(prefers-color-scheme: dark)', color: '#0c0c0c' },
@@ -20,7 +18,7 @@ export const metadata = {
metadataBase: new URL('https://docs.sim.ai'),
title: {
default: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce',
template: '%s',
template: '%s | Sim Docs',
},
description:
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',

File diff suppressed because one or more lines are too long

View File

@@ -45,6 +45,7 @@ import {
EnrichSoIcon,
EvernoteIcon,
ExaAIIcon,
ExtendIcon,
EyeIcon,
FathomIcon,
FirecrawlIcon,
@@ -74,6 +75,7 @@ import {
GoogleVaultIcon,
GrafanaIcon,
GrainIcon,
GranolaIcon,
GreenhouseIcon,
GreptileIcon,
HexIcon,
@@ -88,7 +90,9 @@ import {
JiraIcon,
JiraServiceManagementIcon,
KalshiIcon,
KetchIcon,
LangsmithIcon,
LaunchDarklyIcon,
LemlistIcon,
LinearIcon,
LinkedInIcon,
@@ -124,17 +128,21 @@ import {
PolymarketIcon,
PostgresIcon,
PosthogIcon,
ProfoundIcon,
PulseIcon,
QdrantIcon,
QuiverIcon,
RDSIcon,
RedditIcon,
RedisIcon,
ReductoIcon,
ResendIcon,
RevenueCatIcon,
RipplingIcon,
S3Icon,
SalesforceIcon,
SearchIcon,
SecretsManagerIcon,
SendgridIcon,
SentryIcon,
SerperIcon,
@@ -150,6 +158,7 @@ import {
StagehandIcon,
StripeIcon,
SupabaseIcon,
TailscaleIcon,
TavilyIcon,
TelegramIcon,
TextractIcon,
@@ -216,6 +225,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
enrich: EnrichSoIcon,
evernote: EvernoteIcon,
exa: ExaAIIcon,
extend_v2: ExtendIcon,
fathom: FathomIcon,
file_v3: DocumentIcon,
firecrawl: FirecrawlIcon,
@@ -245,6 +255,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
google_vault: GoogleVaultIcon,
grafana: GrafanaIcon,
grain: GrainIcon,
granola: GranolaIcon,
greenhouse: GreenhouseIcon,
greptile: GreptileIcon,
hex: HexIcon,
@@ -260,8 +271,10 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
jira: JiraIcon,
jira_service_management: JiraServiceManagementIcon,
kalshi_v2: KalshiIcon,
ketch: KetchIcon,
knowledge: PackageSearchIcon,
langsmith: LangsmithIcon,
launchdarkly: LaunchDarklyIcon,
lemlist: LemlistIcon,
linear: LinearIcon,
linkedin: LinkedInIcon,
@@ -296,17 +309,21 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
polymarket: PolymarketIcon,
postgresql: PostgresIcon,
posthog: PosthogIcon,
profound: ProfoundIcon,
pulse_v2: PulseIcon,
qdrant: QdrantIcon,
quiver: QuiverIcon,
rds: RDSIcon,
reddit: RedditIcon,
redis: RedisIcon,
reducto_v2: ReductoIcon,
resend: ResendIcon,
revenuecat: RevenueCatIcon,
rippling: RipplingIcon,
s3: S3Icon,
salesforce: SalesforceIcon,
search: SearchIcon,
secrets_manager: SecretsManagerIcon,
sendgrid: SendgridIcon,
sentry: SentryIcon,
serper: SerperIcon,
@@ -323,6 +340,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
stripe: StripeIcon,
stt_v2: STTIcon,
supabase: SupabaseIcon,
tailscale: TailscaleIcon,
tavily: TavilyIcon,
telegram: TelegramIcon,
textract_v2: TextractIcon,

View File

@@ -131,7 +131,7 @@ Erkennt personenbezogene Daten mithilfe von Microsoft Presidio. Unterstützt üb
**Anwendungsfälle:**
- Blockieren von Inhalten mit sensiblen persönlichen Informationen
- Maskieren von personenbezogenen Daten vor der Protokollierung oder Speicherung
- Einhaltung der DSGVO, HIPAA und anderer Datenschutzbestimmungen
- Einhaltung der DSGVO und anderer Datenschutzbestimmungen
- Bereinigung von Benutzereingaben vor der Verarbeitung
## Konfiguration

View File

@@ -132,7 +132,7 @@ Detects personally identifiable information using Microsoft Presidio. Supports o
**Use Cases:**
- Block content containing sensitive personal information
- Mask PII before logging or storing data
- Compliance with GDPR, HIPAA, and other privacy regulations
- Compliance with GDPR and other privacy regulations
- Sanitize user inputs before processing
## Configuration

View File

@@ -0,0 +1,206 @@
---
title: Google Service Accounts
description: Set up Google service accounts with domain-wide delegation for Gmail, Sheets, Drive, Calendar, and other Google services
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Step, Steps } from 'fumadocs-ui/components/steps'
import { Image } from '@/components/ui/image'
import { FAQ } from '@/components/ui/faq'
Google service accounts with domain-wide delegation let your workflows access Google APIs on behalf of users in your Google Workspace domain — without requiring each user to complete an OAuth consent flow. This is ideal for automated workflows that need to send emails, read spreadsheets, or manage files across your organization.
For example, you could build a workflow that iterates through a list of employees, impersonates each one to read their Google Docs, and uploads the contents to a shared knowledge base — all without requiring any of those users to sign in.
## Prerequisites
Before adding a service account to Sim, you need to configure it in the Google Cloud Console and Google Workspace Admin Console.
### 1. Create a Service Account in Google Cloud
<Steps>
<Step>
Go to the [Google Cloud Console](https://console.cloud.google.com/) and select your project (or create one)
</Step>
<Step>
Navigate to **IAM & Admin** → **Service Accounts**
</Step>
<Step>
Click **Create Service Account**, give it a name and description, then click **Create and Continue**
<div className="flex justify-center">
<Image
src="/static/credentials/gcp-create-service-account.png"
alt="Google Cloud Console — Create service account form"
width={700}
height={500}
className="my-4"
/>
</div>
</Step>
<Step>
Skip the optional role and user access steps and click **Done**
</Step>
<Step>
Click on the newly created service account, go to the **Keys** tab, and click **Add Key** → **Create new key**
</Step>
<Step>
Select **JSON** as the key type and click **Create**. A JSON key file will download — keep this safe
<div className="flex justify-center">
<Image
src="/static/credentials/gcp-create-private-key.png"
alt="Google Cloud Console — Create private key dialog with JSON selected"
width={700}
height={400}
className="my-4"
/>
</div>
</Step>
</Steps>
<Callout type="warn">
The JSON key file contains your service account's private key. Treat it like a password — do not commit it to source control or share it publicly.
</Callout>
### 2. Enable the Required APIs
In the Google Cloud Console, go to **APIs & Services** → **Library** and enable the APIs for the services your workflows will use. See the [scopes reference](#scopes-reference) below for the full list of APIs by service.
### 3. Set Up Domain-Wide Delegation
<Steps>
<Step>
In the Google Cloud Console, go to **IAM & Admin** → **Service Accounts**, click on your service account, and copy the **Client ID** (the numeric ID, not the email)
</Step>
<Step>
Open the [Google Workspace Admin Console](https://admin.google.com/) and navigate to **Security** → **Access and data control** → **API controls**
</Step>
<Step>
Click **Manage Domain Wide Delegation**, then click **Add new**
</Step>
<Step>
Paste the **Client ID** from your service account, then add the OAuth scopes for the services your workflows need. Copy the full scope URLs from the [scopes reference](#scopes-reference) below — only authorize scopes for services you plan to use.
<div className="flex justify-center">
<Image
src="/static/credentials/gcp-add-client-id.png"
alt="Google Workspace Admin Console — Add a new client ID with OAuth scopes"
width={350}
height={300}
className="my-4"
/>
</div>
</Step>
<Step>
Click **Authorize**
</Step>
</Steps>
<Callout type="info">
Domain-wide delegation must be configured by a Google Workspace admin. If you are not an admin, send the Client ID and required scopes to your admin.
</Callout>
### Scopes Reference
The table below lists every Google service that supports service account authentication in Sim, the API to enable in Google Cloud Console, and the delegation scopes to authorize. Copy the scope string for each service you need and paste it into the Google Workspace Admin Console.
<table>
<thead>
<tr>
<th className="whitespace-nowrap">Service</th>
<th className="whitespace-nowrap">API to Enable</th>
<th>Delegation Scopes</th>
</tr>
</thead>
<tbody>
<tr><td>Gmail</td><td>Gmail API</td><td><code>{'https://www.googleapis.com/auth/gmail.send'}</code><br/><code>{'https://www.googleapis.com/auth/gmail.modify'}</code><br/><code>{'https://www.googleapis.com/auth/gmail.labels'}</code></td></tr>
<tr><td>Google Sheets</td><td>Google Sheets API, Google Drive API</td><td><code>{'https://www.googleapis.com/auth/drive'}</code><br/><code>{'https://www.googleapis.com/auth/drive.file'}</code></td></tr>
<tr><td>Google Drive</td><td>Google Drive API</td><td><code>{'https://www.googleapis.com/auth/drive'}</code><br/><code>{'https://www.googleapis.com/auth/drive.file'}</code></td></tr>
<tr><td>Google Docs</td><td>Google Docs API, Google Drive API</td><td><code>{'https://www.googleapis.com/auth/drive'}</code><br/><code>{'https://www.googleapis.com/auth/drive.file'}</code></td></tr>
<tr><td>Google Slides</td><td>Google Slides API, Google Drive API</td><td><code>{'https://www.googleapis.com/auth/drive'}</code><br/><code>{'https://www.googleapis.com/auth/drive.file'}</code></td></tr>
<tr><td>Google Forms</td><td>Google Forms API, Google Drive API</td><td><code>{'https://www.googleapis.com/auth/drive'}</code><br/><code>{'https://www.googleapis.com/auth/forms.body'}</code><br/><code>{'https://www.googleapis.com/auth/forms.responses.readonly'}</code></td></tr>
<tr><td>Google Calendar</td><td>Google Calendar API</td><td><code>{'https://www.googleapis.com/auth/calendar'}</code></td></tr>
<tr><td>Google Contacts</td><td>People API</td><td><code>{'https://www.googleapis.com/auth/contacts'}</code></td></tr>
<tr><td>BigQuery</td><td>BigQuery API</td><td><code>{'https://www.googleapis.com/auth/bigquery'}</code></td></tr>
<tr><td>Google Tasks</td><td>Tasks API</td><td><code>{'https://www.googleapis.com/auth/tasks'}</code></td></tr>
<tr><td>Google Vault</td><td>Vault API, Cloud Storage API</td><td><code>{'https://www.googleapis.com/auth/ediscovery'}</code><br/><code>{'https://www.googleapis.com/auth/devstorage.read_only'}</code></td></tr>
<tr><td>Google Groups</td><td>Admin SDK API</td><td><code>{'https://www.googleapis.com/auth/admin.directory.group'}</code><br/><code>{'https://www.googleapis.com/auth/admin.directory.group.member'}</code></td></tr>
<tr><td>Google Meet</td><td>Google Meet API</td><td><code>{'https://www.googleapis.com/auth/meetings.space.created'}</code><br/><code>{'https://www.googleapis.com/auth/meetings.space.readonly'}</code></td></tr>
</tbody>
</table>
<Callout type="info">
You only need to enable APIs and authorize scopes for the services you plan to use. When authorizing multiple services, combine their scope strings with commas into a single entry in the Admin Console.
</Callout>
## Adding the Service Account to Sim
Once Google Cloud and Workspace are configured, add the service account as a credential in Sim.
<Steps>
<Step>
Open your workspace **Settings** and go to the **Integrations** tab
</Step>
<Step>
Search for "Google Service Account" and click **Connect**
<div className="flex justify-center">
<Image
src="/static/credentials/integrations-service-account.png"
alt="Integrations page showing Google Service Account"
width={800}
height={150}
className="my-4"
/>
</div>
</Step>
<Step>
Paste the full contents of your JSON key file into the text area
<div className="flex justify-center">
<Image
src="/static/credentials/add-service-account.png"
alt="Add Google Service Account dialog"
width={350}
height={420}
className="my-6"
/>
</div>
</Step>
<Step>
Give the credential a display name (the service account email is used by default)
</Step>
<Step>
Click **Save**
</Step>
</Steps>
The JSON key file is validated for the required fields (`type`, `client_email`, `private_key`, `project_id`) and encrypted before being stored.
## Using Delegated Access in Workflows
When you use a Google block (Gmail, Sheets, Drive, etc.) in a workflow and select a service account credential, an **Impersonate User Email** field appears below the credential selector.
Enter the email address of the Google Workspace user you want the service account to act as. For example, if you enter `alice@yourcompany.com`, the workflow will send emails from Alice's account, read her spreadsheets, or access her calendar — depending on the scopes you authorized.
<div className="flex justify-center">
<Image
src="/static/credentials/workflow-impersonated-account.png"
alt="Gmail block in a workflow showing the Impersonated Account field with a service account credential"
width={800}
height={350}
className="my-4"
/>
</div>
<Callout type="warn">
The impersonated email must belong to a user in the Google Workspace domain where you configured domain-wide delegation. Impersonating external email addresses will fail.
</Callout>
<FAQ items={[
{ question: "Can I use a service account without domain-wide delegation?", answer: "Yes, but it will only be able to access resources owned by the service account itself (e.g., spreadsheets shared directly with the service account email). Without delegation, you cannot impersonate users or access their personal data like Gmail." },
{ question: "What happens if the impersonation email field is left blank?", answer: "The service account will authenticate as itself. This works for accessing shared resources (like a Google Sheet shared with the service account email) but will fail for user-specific APIs like Gmail." },
{ question: "Can I use the same service account for multiple Google services?", answer: "Yes. A single service account can be used across Gmail, Sheets, Drive, Calendar, and other Google services — as long as the required API is enabled in Google Cloud and the corresponding scopes are authorized in the Workspace admin console." },
{ question: "How do I rotate the service account key?", answer: "Create a new JSON key in the Google Cloud Console under your service account's Keys tab, then update the credential in Sim with the new key. Delete the old key from Google Cloud once the new one is working." },
{ question: "Does the impersonated user need a Google Workspace license?", answer: "Yes. Domain-wide delegation only works with users who have a Google Workspace account in the domain. Consumer Gmail accounts (e.g., @gmail.com) cannot be impersonated." },
]} />

View File

@@ -0,0 +1,5 @@
{
"title": "Credentials",
"pages": ["index", "google-service-account"],
"defaultOpen": false
}

View File

@@ -195,6 +195,17 @@ By default, your usage is capped at the credits included in your plan. To allow
Max (individual) shares the same rate limits as team plans. Team plans (Pro or Max for Teams) use the Max-tier rate limits.
### Concurrent Execution Limits
| Plan | Concurrent Executions |
|------|----------------------|
| **Free** | 5 |
| **Pro** | 50 |
| **Max / Team** | 200 |
| **Enterprise** | 200 (customizable) |
Concurrent execution limits control how many workflow executions can run simultaneously within a workspace. When the limit is reached, new executions are queued and admitted as running executions complete. Manual runs from the editor are not subject to these limits.
### File Storage
| Plan | Storage |

View File

@@ -359,6 +359,35 @@ List tasks in Attio, optionally filtered by record, assignee, or completion stat
| ↳ `createdAt` | string | When the task was created |
| `count` | number | Number of tasks returned |
### `attio_get_task`
Get a single task by ID from Attio
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `taskId` | string | Yes | The ID of the task to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `taskId` | string | The task ID |
| `content` | string | The task content |
| `deadlineAt` | string | The task deadline |
| `isCompleted` | boolean | Whether the task is completed |
| `linkedRecords` | array | Records linked to this task |
| ↳ `targetObjectId` | string | The linked object ID |
| ↳ `targetRecordId` | string | The linked record ID |
| `assignees` | array | Task assignees |
| ↳ `type` | string | The assignee actor type \(e.g. workspace-member\) |
| ↳ `id` | string | The assignee actor ID |
| `createdByActor` | object | The actor who created this task |
| ↳ `type` | string | The actor type \(e.g. workspace-member, api-token, system\) |
| ↳ `id` | string | The actor ID |
| `createdAt` | string | When the task was created |
### `attio_create_task`
Create a task in Attio
@@ -1012,8 +1041,8 @@ Update a webhook in Attio (target URL and/or subscriptions)
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `webhookId` | string | Yes | The webhook ID to update |
| `targetUrl` | string | Yes | HTTPS target URL for webhook delivery |
| `subscriptions` | string | Yes | JSON array of subscriptions, e.g. \[\{"event_type":"note.created"\}\] |
| `targetUrl` | string | No | HTTPS target URL for webhook delivery |
| `subscriptions` | string | No | JSON array of subscriptions, e.g. \[\{"event_type":"note.created"\}\] |
#### Output

View File

@@ -0,0 +1,39 @@
---
title: Extend
description: Parse and extract content from documents
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="extend_v2"
color="#000000"
/>
## Usage Instructions
Integrate Extend AI into the workflow. Parse and extract structured content from documents or file references.
## Tools
### `extend_parser`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `filePath` | string | No | URL to a document to be processed |
| `file` | file | No | Document file to be processed |
| `fileUpload` | object | No | File upload data from file-upload component |
| `outputFormat` | string | No | Target output format \(markdown or spatial\). Defaults to markdown. |
| `chunking` | string | No | Chunking strategy \(page, document, or section\). Defaults to page. |
| `engine` | string | No | Parsing engine \(parse_performance or parse_light\). Defaults to parse_performance. |
| `apiKey` | string | Yes | Extend API key |
#### Output
This tool does not produce any outputs.

View File

@@ -1,6 +1,6 @@
---
title: File
description: Read and parse multiple files
description: Read and write workspace files
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
@@ -27,7 +27,7 @@ The File Parser tool is particularly useful for scenarios where your agents need
## Usage Instructions
Upload files directly or import from external URLs to get UserFile objects for use in other blocks.
Read and parse files from uploads or URLs, write new workspace files, or append content to existing files.
@@ -52,4 +52,45 @@ Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc
| `files` | file[] | Parsed files as UserFile objects |
| `combinedContent` | string | Combined content of all parsed files |
### `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 |

View File

@@ -0,0 +1,92 @@
---
title: Granola
description: Access meeting notes and transcripts from Granola
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="granola"
color="#B2C147"
/>
## Usage Instructions
Integrate Granola into your workflow to retrieve meeting notes, summaries, attendees, and transcripts.
## Tools
### `granola_list_notes`
Lists meeting notes from Granola with optional date filters and pagination.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Granola API key |
| `createdBefore` | string | No | Return notes created before this date \(ISO 8601\) |
| `createdAfter` | string | No | Return notes created after this date \(ISO 8601\) |
| `updatedAfter` | string | No | Return notes updated after this date \(ISO 8601\) |
| `cursor` | string | No | Pagination cursor from a previous response |
| `pageSize` | number | No | Number of notes per page \(1-30, default 10\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `notes` | json | List of meeting notes |
| ↳ `id` | string | Note ID |
| ↳ `title` | string | Note title |
| ↳ `ownerName` | string | Note owner name |
| ↳ `ownerEmail` | string | Note owner email |
| ↳ `createdAt` | string | Creation timestamp |
| ↳ `updatedAt` | string | Last update timestamp |
| `hasMore` | boolean | Whether more notes are available |
| `cursor` | string | Pagination cursor for the next page |
### `granola_get_note`
Retrieves a specific meeting note from Granola by ID, including summary, attendees, calendar event details, and optionally the transcript.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Granola API key |
| `noteId` | string | Yes | The note ID \(e.g., not_1d3tmYTlCICgjy\) |
| `includeTranscript` | string | No | Whether to include the meeting transcript |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Note ID |
| `title` | string | Note title |
| `ownerName` | string | Note owner name |
| `ownerEmail` | string | Note owner email |
| `createdAt` | string | Creation timestamp |
| `updatedAt` | string | Last update timestamp |
| `summaryText` | string | Plain text summary of the meeting |
| `summaryMarkdown` | string | Markdown-formatted summary of the meeting |
| `attendees` | json | Meeting attendees |
| ↳ `name` | string | Attendee name |
| ↳ `email` | string | Attendee email |
| `folders` | json | Folders the note belongs to |
| ↳ `id` | string | Folder ID |
| ↳ `name` | string | Folder name |
| `calendarEventTitle` | string | Calendar event title |
| `calendarOrganiser` | string | Calendar event organiser email |
| `calendarEventId` | string | Calendar event ID |
| `scheduledStartTime` | string | Scheduled start time |
| `scheduledEndTime` | string | Scheduled end time |
| `invitees` | json | Calendar event invitee emails |
| `transcript` | json | Meeting transcript entries \(only if requested\) |
| ↳ `speaker` | string | Speaker source \(microphone or speaker\) |
| ↳ `text` | string | Transcript text |
| ↳ `startTime` | string | Segment start time |
| ↳ `endTime` | string | Segment end time |

View File

@@ -41,6 +41,7 @@ Retrieve all users from HubSpot account
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `limit` | string | No | Number of results to return \(default: 100, max: 100\) |
| `after` | string | No | Pagination cursor for next page of results \(from previous response\) |
#### Output
@@ -53,6 +54,9 @@ Retrieve all users from HubSpot account
| ↳ `primaryTeamId` | string | Primary team ID |
| ↳ `secondaryTeamIds` | array | Secondary team IDs |
| ↳ `superAdmin` | boolean | Whether user is a super admin |
| `paging` | object | Pagination information for fetching more results |
| ↳ `after` | string | Cursor for next page of results |
| ↳ `link` | string | Link to next page |
| `totalItems` | number | Total number of users returned |
| `success` | boolean | Operation success status |
@@ -230,7 +234,7 @@ Search for contacts in HubSpot using filters, sorting, and queries
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `filterGroups` | array | No | Array of filter groups as JSON. Each group contains "filters" array with objects having "propertyName", "operator" \(e.g., "EQ", "CONTAINS"\), and "value" |
| `filterGroups` | array | No | Array of filter groups as JSON. Each group contains "filters" array with objects having "propertyName", "operator" \(e.g., "EQ", "CONTAINS_TOKEN", "GT"\), and "value" |
| `sorts` | array | No | Array of sort objects as JSON with "propertyName" and "direction" \("ASCENDING" or "DESCENDING"\) |
| `query` | string | No | Search query string to match against contact name, email, and other text fields |
| `properties` | array | No | Array of HubSpot property names to return \(e.g., \["email", "firstname", "lastname", "phone"\]\) |
@@ -449,7 +453,7 @@ Search for companies in HubSpot using filters, sorting, and queries
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `filterGroups` | array | No | Array of filter groups as JSON. Each group contains "filters" array with objects having "propertyName", "operator" \(e.g., "EQ", "CONTAINS"\), and "value" |
| `filterGroups` | array | No | Array of filter groups as JSON. Each group contains "filters" array with objects having "propertyName", "operator" \(e.g., "EQ", "CONTAINS_TOKEN", "GT"\), and "value" |
| `sorts` | array | No | Array of sort objects as JSON with "propertyName" and "direction" \("ASCENDING" or "DESCENDING"\) |
| `query` | string | No | Search query string to match against company name, domain, and other text fields |
| `properties` | array | No | Array of HubSpot property names to return \(e.g., \["name", "domain", "industry"\]\) |
@@ -499,7 +503,7 @@ Retrieve all deals from HubSpot account with pagination support
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `limit` | string | No | Maximum number of results per page \(max 100, default 100\) |
| `limit` | string | No | Maximum number of results per page \(max 100, default 10\) |
| `after` | string | No | Pagination cursor for next page of results \(from previous response\) |
| `properties` | string | No | Comma-separated list of HubSpot property names to return \(e.g., "dealname,amount,dealstage"\) |
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for \(e.g., "contacts,companies"\) |
@@ -529,4 +533,887 @@ Retrieve all deals from HubSpot account with pagination support
| ↳ `hasMore` | boolean | Whether more records are available |
| `success` | boolean | Operation success status |
### `hubspot_get_deal`
Retrieve a single deal by ID from HubSpot
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `dealId` | string | Yes | The HubSpot deal ID to retrieve |
| `idProperty` | string | No | Property to use as unique identifier. If not specified, uses record ID |
| `properties` | string | No | Comma-separated list of HubSpot property names to return \(e.g., "dealname,amount,dealstage"\) |
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for \(e.g., "contacts,companies"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `deal` | object | HubSpot deal record |
| ↳ `dealname` | string | Deal name |
| ↳ `amount` | string | Deal amount |
| ↳ `dealstage` | string | Current deal stage |
| ↳ `pipeline` | string | Pipeline the deal is in |
| ↳ `closedate` | string | Expected close date \(ISO 8601\) |
| ↳ `dealtype` | string | Deal type \(New Business, Existing Business, etc.\) |
| ↳ `description` | string | Deal description |
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
| ↳ `createdate` | string | Deal creation date \(ISO 8601\) |
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
| ↳ `num_associated_contacts` | string | Number of associated contacts |
| `dealId` | string | The retrieved deal ID |
| `success` | boolean | Operation success status |
### `hubspot_create_deal`
Create a new deal in HubSpot. Requires at least a dealname property
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `properties` | object | Yes | Deal properties as JSON object. Must include dealname \(e.g., \{"dealname": "New Deal", "amount": "5000", "dealstage": "appointmentscheduled"\}\) |
| `associations` | array | No | Array of associations to create with the deal as JSON. Each object should have "to.id" and "types" array with "associationCategory" and "associationTypeId" |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `deal` | object | HubSpot deal record |
| ↳ `dealname` | string | Deal name |
| ↳ `amount` | string | Deal amount |
| ↳ `dealstage` | string | Current deal stage |
| ↳ `pipeline` | string | Pipeline the deal is in |
| ↳ `closedate` | string | Expected close date \(ISO 8601\) |
| ↳ `dealtype` | string | Deal type \(New Business, Existing Business, etc.\) |
| ↳ `description` | string | Deal description |
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
| ↳ `createdate` | string | Deal creation date \(ISO 8601\) |
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
| ↳ `num_associated_contacts` | string | Number of associated contacts |
| `dealId` | string | The created deal ID |
| `success` | boolean | Operation success status |
### `hubspot_update_deal`
Update an existing deal in HubSpot by ID
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `dealId` | string | Yes | The HubSpot deal ID to update |
| `idProperty` | string | No | Property to use as unique identifier. If not specified, uses record ID |
| `properties` | object | Yes | Deal properties to update as JSON object \(e.g., \{"amount": "10000", "dealstage": "closedwon"\}\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `deal` | object | HubSpot deal record |
| ↳ `dealname` | string | Deal name |
| ↳ `amount` | string | Deal amount |
| ↳ `dealstage` | string | Current deal stage |
| ↳ `pipeline` | string | Pipeline the deal is in |
| ↳ `closedate` | string | Expected close date \(ISO 8601\) |
| ↳ `dealtype` | string | Deal type \(New Business, Existing Business, etc.\) |
| ↳ `description` | string | Deal description |
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
| ↳ `createdate` | string | Deal creation date \(ISO 8601\) |
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
| ↳ `num_associated_contacts` | string | Number of associated contacts |
| `dealId` | string | The updated deal ID |
| `success` | boolean | Operation success status |
### `hubspot_search_deals`
Search for deals in HubSpot using filters, sorting, and queries
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `filterGroups` | array | No | Array of filter groups as JSON. Each group contains "filters" array with objects having "propertyName", "operator" \(e.g., "EQ", "NEQ", "CONTAINS_TOKEN", "NOT_CONTAINS_TOKEN"\), and "value" |
| `sorts` | array | No | Array of sort objects as JSON with "propertyName" and "direction" \("ASCENDING" or "DESCENDING"\) |
| `query` | string | No | Search query string to match against deal name and other text fields |
| `properties` | array | No | Array of HubSpot property names to return \(e.g., \["dealname", "amount", "dealstage"\]\) |
| `limit` | number | No | Maximum number of results to return \(max 200\) |
| `after` | string | No | Pagination cursor for next page \(from previous response\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `deals` | array | Array of HubSpot deal records |
| ↳ `dealname` | string | Deal name |
| ↳ `amount` | string | Deal amount |
| ↳ `dealstage` | string | Current deal stage |
| ↳ `pipeline` | string | Pipeline the deal is in |
| ↳ `closedate` | string | Expected close date \(ISO 8601\) |
| ↳ `dealtype` | string | Deal type \(New Business, Existing Business, etc.\) |
| ↳ `description` | string | Deal description |
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
| ↳ `createdate` | string | Deal creation date \(ISO 8601\) |
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
| ↳ `num_associated_contacts` | string | Number of associated contacts |
| `paging` | object | Pagination information for fetching more results |
| ↳ `after` | string | Cursor for next page of results |
| ↳ `link` | string | Link to next page |
| `metadata` | object | Response metadata |
| ↳ `totalReturned` | number | Number of records returned in this response |
| ↳ `hasMore` | boolean | Whether more records are available |
| `total` | number | Total number of matching deals |
| `success` | boolean | Operation success status |
### `hubspot_list_tickets`
Retrieve all tickets from HubSpot account with pagination support
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `limit` | string | No | Maximum number of results per page \(max 100, default 10\) |
| `after` | string | No | Pagination cursor for next page of results \(from previous response\) |
| `properties` | string | No | Comma-separated list of HubSpot property names to return \(e.g., "subject,content,hs_ticket_priority"\) |
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for \(e.g., "contacts,companies"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `tickets` | array | Array of HubSpot ticket records |
| ↳ `subject` | string | Ticket subject/name |
| ↳ `content` | string | Ticket content/description |
| ↳ `hs_pipeline` | string | Pipeline the ticket is in |
| ↳ `hs_pipeline_stage` | string | Current pipeline stage |
| ↳ `hs_ticket_priority` | string | Ticket priority \(LOW, MEDIUM, HIGH\) |
| ↳ `hs_ticket_category` | string | Ticket category |
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
| ↳ `createdate` | string | Ticket creation date \(ISO 8601\) |
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
| `paging` | object | Pagination information for fetching more results |
| ↳ `after` | string | Cursor for next page of results |
| ↳ `link` | string | Link to next page |
| `metadata` | object | Response metadata |
| ↳ `totalReturned` | number | Number of records returned in this response |
| ↳ `hasMore` | boolean | Whether more records are available |
| `success` | boolean | Operation success status |
### `hubspot_get_ticket`
Retrieve a single ticket by ID from HubSpot
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `ticketId` | string | Yes | The HubSpot ticket ID to retrieve |
| `idProperty` | string | No | Property to use as unique identifier. If not specified, uses record ID |
| `properties` | string | No | Comma-separated list of HubSpot property names to return \(e.g., "subject,content,hs_ticket_priority"\) |
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for \(e.g., "contacts,companies"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ticket` | object | HubSpot ticket record |
| ↳ `subject` | string | Ticket subject/name |
| ↳ `content` | string | Ticket content/description |
| ↳ `hs_pipeline` | string | Pipeline the ticket is in |
| ↳ `hs_pipeline_stage` | string | Current pipeline stage |
| ↳ `hs_ticket_priority` | string | Ticket priority \(LOW, MEDIUM, HIGH\) |
| ↳ `hs_ticket_category` | string | Ticket category |
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
| ↳ `createdate` | string | Ticket creation date \(ISO 8601\) |
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
| `ticketId` | string | The retrieved ticket ID |
| `success` | boolean | Operation success status |
### `hubspot_create_ticket`
Create a new ticket in HubSpot. Requires subject and hs_pipeline_stage properties
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `properties` | object | Yes | Ticket properties as JSON object. Must include subject and hs_pipeline_stage \(e.g., \{"subject": "Support request", "hs_pipeline_stage": "1", "hs_ticket_priority": "HIGH"\}\) |
| `associations` | array | No | Array of associations to create with the ticket as JSON. Each object should have "to.id" and "types" array with "associationCategory" and "associationTypeId" |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ticket` | object | HubSpot ticket record |
| ↳ `subject` | string | Ticket subject/name |
| ↳ `content` | string | Ticket content/description |
| ↳ `hs_pipeline` | string | Pipeline the ticket is in |
| ↳ `hs_pipeline_stage` | string | Current pipeline stage |
| ↳ `hs_ticket_priority` | string | Ticket priority \(LOW, MEDIUM, HIGH\) |
| ↳ `hs_ticket_category` | string | Ticket category |
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
| ↳ `createdate` | string | Ticket creation date \(ISO 8601\) |
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
| `ticketId` | string | The created ticket ID |
| `success` | boolean | Operation success status |
### `hubspot_update_ticket`
Update an existing ticket in HubSpot by ID
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `ticketId` | string | Yes | The HubSpot ticket ID to update |
| `idProperty` | string | No | Property to use as unique identifier. If not specified, uses record ID |
| `properties` | object | Yes | Ticket properties to update as JSON object \(e.g., \{"subject": "Updated subject", "hs_ticket_priority": "HIGH"\}\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ticket` | object | HubSpot ticket record |
| ↳ `subject` | string | Ticket subject/name |
| ↳ `content` | string | Ticket content/description |
| ↳ `hs_pipeline` | string | Pipeline the ticket is in |
| ↳ `hs_pipeline_stage` | string | Current pipeline stage |
| ↳ `hs_ticket_priority` | string | Ticket priority \(LOW, MEDIUM, HIGH\) |
| ↳ `hs_ticket_category` | string | Ticket category |
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
| ↳ `createdate` | string | Ticket creation date \(ISO 8601\) |
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
| `ticketId` | string | The updated ticket ID |
| `success` | boolean | Operation success status |
### `hubspot_search_tickets`
Search for tickets in HubSpot using filters, sorting, and queries
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `filterGroups` | array | No | Array of filter groups as JSON. Each group contains "filters" array with objects having "propertyName", "operator" \(e.g., "EQ", "NEQ", "CONTAINS_TOKEN", "NOT_CONTAINS_TOKEN"\), and "value" |
| `sorts` | array | No | Array of sort objects as JSON with "propertyName" and "direction" \("ASCENDING" or "DESCENDING"\) |
| `query` | string | No | Search query string to match against ticket subject and other text fields |
| `properties` | array | No | Array of HubSpot property names to return \(e.g., \["subject", "content", "hs_ticket_priority"\]\) |
| `limit` | number | No | Maximum number of results to return \(max 200\) |
| `after` | string | No | Pagination cursor for next page \(from previous response\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `tickets` | array | Array of HubSpot ticket records |
| ↳ `subject` | string | Ticket subject/name |
| ↳ `content` | string | Ticket content/description |
| ↳ `hs_pipeline` | string | Pipeline the ticket is in |
| ↳ `hs_pipeline_stage` | string | Current pipeline stage |
| ↳ `hs_ticket_priority` | string | Ticket priority \(LOW, MEDIUM, HIGH\) |
| ↳ `hs_ticket_category` | string | Ticket category |
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
| ↳ `createdate` | string | Ticket creation date \(ISO 8601\) |
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
| `paging` | object | Pagination information for fetching more results |
| ↳ `after` | string | Cursor for next page of results |
| ↳ `link` | string | Link to next page |
| `metadata` | object | Response metadata |
| ↳ `totalReturned` | number | Number of records returned in this response |
| ↳ `hasMore` | boolean | Whether more records are available |
| `total` | number | Total number of matching tickets |
| `success` | boolean | Operation success status |
### `hubspot_list_line_items`
Retrieve all line items from HubSpot account with pagination support
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `limit` | string | No | Maximum number of results per page \(max 100, default 10\) |
| `after` | string | No | Pagination cursor for next page of results \(from previous response\) |
| `properties` | string | No | Comma-separated list of HubSpot property names to return \(e.g., "name,quantity,price,amount"\) |
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for \(e.g., "deals,quotes"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `lineItems` | array | Array of HubSpot line item records |
| ↳ `name` | string | Line item name |
| ↳ `description` | string | Full description of the product |
| ↳ `hs_sku` | string | Unique product identifier \(SKU\) |
| ↳ `quantity` | string | Number of units included |
| ↳ `price` | string | Unit price |
| ↳ `amount` | string | Total cost \(quantity * unit price\) |
| ↳ `hs_line_item_currency_code` | string | Currency code |
| ↳ `recurringbillingfrequency` | string | Recurring billing frequency |
| ↳ `hs_recurring_billing_start_date` | string | Recurring billing start date |
| ↳ `hs_recurring_billing_end_date` | string | Recurring billing end date |
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
| ↳ `createdate` | string | Creation date \(ISO 8601\) |
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
| `paging` | object | Pagination information for fetching more results |
| ↳ `after` | string | Cursor for next page of results |
| ↳ `link` | string | Link to next page |
| `metadata` | object | Response metadata |
| ↳ `totalReturned` | number | Number of records returned in this response |
| ↳ `hasMore` | boolean | Whether more records are available |
| `success` | boolean | Operation success status |
### `hubspot_get_line_item`
Retrieve a single line item by ID from HubSpot
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `lineItemId` | string | Yes | The HubSpot line item ID to retrieve |
| `idProperty` | string | No | Property to use as unique identifier. If not specified, uses record ID |
| `properties` | string | No | Comma-separated list of HubSpot property names to return \(e.g., "name,quantity,price,amount"\) |
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for \(e.g., "deals,quotes"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `lineItem` | object | HubSpot line item record |
| ↳ `name` | string | Line item name |
| ↳ `description` | string | Full description of the product |
| ↳ `hs_sku` | string | Unique product identifier \(SKU\) |
| ↳ `quantity` | string | Number of units included |
| ↳ `price` | string | Unit price |
| ↳ `amount` | string | Total cost \(quantity * unit price\) |
| ↳ `hs_line_item_currency_code` | string | Currency code |
| ↳ `recurringbillingfrequency` | string | Recurring billing frequency |
| ↳ `hs_recurring_billing_start_date` | string | Recurring billing start date |
| ↳ `hs_recurring_billing_end_date` | string | Recurring billing end date |
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
| ↳ `createdate` | string | Creation date \(ISO 8601\) |
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
| `lineItemId` | string | The retrieved line item ID |
| `success` | boolean | Operation success status |
### `hubspot_create_line_item`
Create a new line item in HubSpot. Requires at least a name property
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `properties` | object | Yes | Line item properties as JSON object \(e.g., \{"name": "Product A", "quantity": "2", "price": "50.00", "hs_sku": "SKU-001"\}\) |
| `associations` | array | No | Array of associations to create with the line item as JSON. Each object should have "to.id" and "types" array with "associationCategory" and "associationTypeId" |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `lineItem` | object | HubSpot line item record |
| ↳ `name` | string | Line item name |
| ↳ `description` | string | Full description of the product |
| ↳ `hs_sku` | string | Unique product identifier \(SKU\) |
| ↳ `quantity` | string | Number of units included |
| ↳ `price` | string | Unit price |
| ↳ `amount` | string | Total cost \(quantity * unit price\) |
| ↳ `hs_line_item_currency_code` | string | Currency code |
| ↳ `recurringbillingfrequency` | string | Recurring billing frequency |
| ↳ `hs_recurring_billing_start_date` | string | Recurring billing start date |
| ↳ `hs_recurring_billing_end_date` | string | Recurring billing end date |
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
| ↳ `createdate` | string | Creation date \(ISO 8601\) |
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
| `lineItemId` | string | The created line item ID |
| `success` | boolean | Operation success status |
### `hubspot_update_line_item`
Update an existing line item in HubSpot by ID
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `lineItemId` | string | Yes | The HubSpot line item ID to update |
| `idProperty` | string | No | Property to use as unique identifier. If not specified, uses record ID |
| `properties` | object | Yes | Line item properties to update as JSON object \(e.g., \{"quantity": "5", "price": "25.00"\}\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `lineItem` | object | HubSpot line item record |
| ↳ `name` | string | Line item name |
| ↳ `description` | string | Full description of the product |
| ↳ `hs_sku` | string | Unique product identifier \(SKU\) |
| ↳ `quantity` | string | Number of units included |
| ↳ `price` | string | Unit price |
| ↳ `amount` | string | Total cost \(quantity * unit price\) |
| ↳ `hs_line_item_currency_code` | string | Currency code |
| ↳ `recurringbillingfrequency` | string | Recurring billing frequency |
| ↳ `hs_recurring_billing_start_date` | string | Recurring billing start date |
| ↳ `hs_recurring_billing_end_date` | string | Recurring billing end date |
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
| ↳ `createdate` | string | Creation date \(ISO 8601\) |
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
| `lineItemId` | string | The updated line item ID |
| `success` | boolean | Operation success status |
### `hubspot_list_quotes`
Retrieve all quotes from HubSpot account with pagination support
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `limit` | string | No | Maximum number of results per page \(max 100, default 10\) |
| `after` | string | No | Pagination cursor for next page of results \(from previous response\) |
| `properties` | string | No | Comma-separated list of HubSpot property names to return \(e.g., "hs_title,hs_expiration_date,hs_status"\) |
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for \(e.g., "deals,line_items"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `quotes` | array | Array of HubSpot quote records |
| ↳ `hs_title` | string | Quote name/title |
| ↳ `hs_expiration_date` | string | Expiration date |
| ↳ `hs_status` | string | Quote status |
| ↳ `hs_esign_enabled` | string | Whether e-signatures are enabled |
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
| ↳ `createdate` | string | Creation date \(ISO 8601\) |
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
| `paging` | object | Pagination information for fetching more results |
| ↳ `after` | string | Cursor for next page of results |
| ↳ `link` | string | Link to next page |
| `metadata` | object | Response metadata |
| ↳ `totalReturned` | number | Number of records returned in this response |
| ↳ `hasMore` | boolean | Whether more records are available |
| `success` | boolean | Operation success status |
### `hubspot_get_quote`
Retrieve a single quote by ID from HubSpot
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `quoteId` | string | Yes | The HubSpot quote ID to retrieve |
| `idProperty` | string | No | Property to use as unique identifier. If not specified, uses record ID |
| `properties` | string | No | Comma-separated list of HubSpot property names to return \(e.g., "hs_title,hs_expiration_date,hs_status"\) |
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for \(e.g., "deals,line_items"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `quote` | object | HubSpot quote record |
| ↳ `hs_title` | string | Quote name/title |
| ↳ `hs_expiration_date` | string | Expiration date |
| ↳ `hs_status` | string | Quote status |
| ↳ `hs_esign_enabled` | string | Whether e-signatures are enabled |
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
| ↳ `createdate` | string | Creation date \(ISO 8601\) |
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
| `quoteId` | string | The retrieved quote ID |
| `success` | boolean | Operation success status |
### `hubspot_list_appointments`
Retrieve all appointments from HubSpot account with pagination support
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `limit` | string | No | Maximum number of results per page \(max 100, default 10\) |
| `after` | string | No | Pagination cursor for next page of results \(from previous response\) |
| `properties` | string | No | Comma-separated list of HubSpot property names to return \(e.g., "hs_meeting_title,hs_meeting_start_time"\) |
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for \(e.g., "contacts,companies"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `appointments` | array | Array of HubSpot appointment records |
| ↳ `hs_appointment_type` | string | Appointment type |
| ↳ `hs_meeting_title` | string | Meeting title |
| ↳ `hs_meeting_start_time` | string | Start time \(ISO 8601\) |
| ↳ `hs_meeting_end_time` | string | End time \(ISO 8601\) |
| ↳ `hs_meeting_location` | string | Meeting location |
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
| ↳ `hs_createdate` | string | Creation date \(ISO 8601\) |
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
| `paging` | object | Pagination information for fetching more results |
| ↳ `after` | string | Cursor for next page of results |
| ↳ `link` | string | Link to next page |
| `metadata` | object | Response metadata |
| ↳ `totalReturned` | number | Number of records returned in this response |
| ↳ `hasMore` | boolean | Whether more records are available |
| `success` | boolean | Operation success status |
### `hubspot_get_appointment`
Retrieve a single appointment by ID from HubSpot
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `appointmentId` | string | Yes | The HubSpot appointment ID to retrieve |
| `idProperty` | string | No | Property to use as unique identifier. If not specified, uses record ID |
| `properties` | string | No | Comma-separated list of HubSpot property names to return \(e.g., "hs_meeting_title,hs_meeting_start_time"\) |
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for \(e.g., "contacts,companies"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `appointment` | object | HubSpot appointment record |
| ↳ `hs_appointment_type` | string | Appointment type |
| ↳ `hs_meeting_title` | string | Meeting title |
| ↳ `hs_meeting_start_time` | string | Start time \(ISO 8601\) |
| ↳ `hs_meeting_end_time` | string | End time \(ISO 8601\) |
| ↳ `hs_meeting_location` | string | Meeting location |
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
| ↳ `hs_createdate` | string | Creation date \(ISO 8601\) |
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
| `appointmentId` | string | The retrieved appointment ID |
| `success` | boolean | Operation success status |
### `hubspot_create_appointment`
Create a new appointment in HubSpot
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `properties` | object | Yes | Appointment properties as JSON object \(e.g., \{"hs_meeting_title": "Discovery Call", "hs_meeting_start_time": "2024-01-15T10:00:00Z", "hs_meeting_end_time": "2024-01-15T11:00:00Z"\}\) |
| `associations` | array | No | Array of associations to create with the appointment as JSON. Each object should have "to.id" and "types" array with "associationCategory" and "associationTypeId" |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `appointment` | object | HubSpot appointment record |
| ↳ `hs_appointment_type` | string | Appointment type |
| ↳ `hs_meeting_title` | string | Meeting title |
| ↳ `hs_meeting_start_time` | string | Start time \(ISO 8601\) |
| ↳ `hs_meeting_end_time` | string | End time \(ISO 8601\) |
| ↳ `hs_meeting_location` | string | Meeting location |
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
| ↳ `hs_createdate` | string | Creation date \(ISO 8601\) |
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
| `appointmentId` | string | The created appointment ID |
| `success` | boolean | Operation success status |
### `hubspot_update_appointment`
Update an existing appointment in HubSpot by ID
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `appointmentId` | string | Yes | The HubSpot appointment ID to update |
| `idProperty` | string | No | Property to use as unique identifier. If not specified, uses record ID |
| `properties` | object | Yes | Appointment properties to update as JSON object \(e.g., \{"hs_meeting_title": "Updated Call", "hs_meeting_location": "Zoom"\}\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `appointment` | object | HubSpot appointment record |
| ↳ `hs_appointment_type` | string | Appointment type |
| ↳ `hs_meeting_title` | string | Meeting title |
| ↳ `hs_meeting_start_time` | string | Start time \(ISO 8601\) |
| ↳ `hs_meeting_end_time` | string | End time \(ISO 8601\) |
| ↳ `hs_meeting_location` | string | Meeting location |
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
| ↳ `hs_createdate` | string | Creation date \(ISO 8601\) |
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
| `appointmentId` | string | The updated appointment ID |
| `success` | boolean | Operation success status |
### `hubspot_list_carts`
Retrieve all carts from HubSpot account with pagination support
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `limit` | string | No | Maximum number of results per page \(max 100, default 10\) |
| `after` | string | No | Pagination cursor for next page of results \(from previous response\) |
| `properties` | string | No | Comma-separated list of HubSpot property names to return |
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `carts` | array | Array of HubSpot CRM records |
| ↳ `id` | string | Unique record ID \(hs_object_id\) |
| ↳ `createdAt` | string | Record creation timestamp \(ISO 8601\) |
| ↳ `updatedAt` | string | Record last updated timestamp \(ISO 8601\) |
| ↳ `archived` | boolean | Whether the record is archived |
| ↳ `properties` | object | Record properties |
| ↳ `associations` | object | Associated records |
| `paging` | object | Pagination information for fetching more results |
| ↳ `after` | string | Cursor for next page of results |
| ↳ `link` | string | Link to next page |
| `metadata` | object | Response metadata |
| ↳ `totalReturned` | number | Number of records returned in this response |
| ↳ `hasMore` | boolean | Whether more records are available |
| `success` | boolean | Operation success status |
### `hubspot_get_cart`
Retrieve a single cart by ID from HubSpot
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `cartId` | string | Yes | The HubSpot cart ID to retrieve |
| `properties` | string | No | Comma-separated list of HubSpot property names to return |
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `cart` | object | HubSpot CRM record |
| ↳ `id` | string | Unique record ID \(hs_object_id\) |
| ↳ `createdAt` | string | Record creation timestamp \(ISO 8601\) |
| ↳ `updatedAt` | string | Record last updated timestamp \(ISO 8601\) |
| ↳ `archived` | boolean | Whether the record is archived |
| ↳ `properties` | object | Record properties |
| ↳ `associations` | object | Associated records |
| `cartId` | string | The retrieved cart ID |
| `success` | boolean | Operation success status |
### `hubspot_list_owners`
Retrieve all owners from HubSpot account with pagination support
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `limit` | string | No | Maximum number of results per page \(max 100, default 100\) |
| `after` | string | No | Pagination cursor for next page of results \(from previous response\) |
| `email` | string | No | Filter owners by email address |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `owners` | array | Array of HubSpot owner objects |
| ↳ `id` | string | Owner ID |
| ↳ `email` | string | Owner email address |
| ↳ `firstName` | string | Owner first name |
| ↳ `lastName` | string | Owner last name |
| ↳ `userId` | number | Associated user ID |
| ↳ `teams` | array | Teams the owner belongs to |
| ↳ `id` | string | Team ID |
| ↳ `name` | string | Team name |
| ↳ `createdAt` | string | Creation date \(ISO 8601\) |
| ↳ `updatedAt` | string | Last updated date \(ISO 8601\) |
| ↳ `archived` | boolean | Whether the owner is archived |
| `paging` | object | Pagination information for fetching more results |
| ↳ `after` | string | Cursor for next page of results |
| ↳ `link` | string | Link to next page |
| `metadata` | object | Response metadata |
| ↳ `totalReturned` | number | Number of records returned in this response |
| ↳ `hasMore` | boolean | Whether more records are available |
| `success` | boolean | Operation success status |
### `hubspot_list_marketing_events`
Retrieve all marketing events from HubSpot account with pagination support
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `limit` | string | No | Maximum number of results per page \(max 100, default 10\) |
| `after` | string | No | Pagination cursor for next page of results \(from previous response\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `events` | array | Array of HubSpot marketing event objects |
| ↳ `objectId` | string | Unique event ID \(HubSpot internal\) |
| ↳ `eventName` | string | Event name |
| ↳ `eventType` | string | Event type |
| ↳ `eventStatus` | string | Event status |
| ↳ `eventDescription` | string | Event description |
| ↳ `eventUrl` | string | Event URL |
| ↳ `eventOrganizer` | string | Event organizer |
| ↳ `startDateTime` | string | Start date/time \(ISO 8601\) |
| ↳ `endDateTime` | string | End date/time \(ISO 8601\) |
| ↳ `eventCancelled` | boolean | Whether event is cancelled |
| ↳ `eventCompleted` | boolean | Whether event is completed |
| ↳ `registrants` | number | Number of registrants |
| ↳ `attendees` | number | Number of attendees |
| ↳ `cancellations` | number | Number of cancellations |
| ↳ `noShows` | number | Number of no-shows |
| ↳ `externalEventId` | string | External event ID |
| ↳ `createdAt` | string | Creation date \(ISO 8601\) |
| ↳ `updatedAt` | string | Last updated date \(ISO 8601\) |
| `paging` | object | Pagination information for fetching more results |
| ↳ `after` | string | Cursor for next page of results |
| ↳ `link` | string | Link to next page |
| `metadata` | object | Response metadata |
| ↳ `totalReturned` | number | Number of records returned in this response |
| ↳ `hasMore` | boolean | Whether more records are available |
| `success` | boolean | Operation success status |
### `hubspot_get_marketing_event`
Retrieve a single marketing event by ID from HubSpot
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `eventId` | string | Yes | The HubSpot marketing event objectId to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `event` | object | HubSpot marketing event |
| ↳ `objectId` | string | Unique event ID \(HubSpot internal\) |
| ↳ `eventName` | string | Event name |
| ↳ `eventType` | string | Event type |
| ↳ `eventStatus` | string | Event status |
| ↳ `eventDescription` | string | Event description |
| ↳ `eventUrl` | string | Event URL |
| ↳ `eventOrganizer` | string | Event organizer |
| ↳ `startDateTime` | string | Start date/time \(ISO 8601\) |
| ↳ `endDateTime` | string | End date/time \(ISO 8601\) |
| ↳ `eventCancelled` | boolean | Whether event is cancelled |
| ↳ `eventCompleted` | boolean | Whether event is completed |
| ↳ `registrants` | number | Number of registrants |
| ↳ `attendees` | number | Number of attendees |
| ↳ `cancellations` | number | Number of cancellations |
| ↳ `noShows` | number | Number of no-shows |
| ↳ `externalEventId` | string | External event ID |
| ↳ `createdAt` | string | Creation date \(ISO 8601\) |
| ↳ `updatedAt` | string | Last updated date \(ISO 8601\) |
| `eventId` | string | The retrieved marketing event ID |
| `success` | boolean | Operation success status |
### `hubspot_list_lists`
Search and retrieve lists from HubSpot account
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `query` | string | No | Search query to filter lists by name. Leave empty to return all lists. |
| `count` | string | No | Maximum number of results to return \(default 20, max 500\) |
| `offset` | string | No | Pagination offset for next page of results \(use the offset value from previous response\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `lists` | array | Array of HubSpot list objects |
| ↳ `listId` | string | List ID |
| ↳ `name` | string | List name |
| ↳ `objectTypeId` | string | Object type ID \(e.g., 0-1 for contacts\) |
| ↳ `processingType` | string | Processing type \(MANUAL, DYNAMIC, SNAPSHOT\) |
| ↳ `processingStatus` | string | Processing status \(COMPLETE, PROCESSING\) |
| ↳ `listVersion` | number | List version number |
| ↳ `createdAt` | string | Creation date \(ISO 8601\) |
| ↳ `updatedAt` | string | Last updated date \(ISO 8601\) |
| `paging` | object | Pagination information for fetching more results |
| ↳ `after` | string | Cursor for next page of results |
| ↳ `link` | string | Link to next page |
| `metadata` | object | Response metadata |
| ↳ `totalReturned` | number | Number of records returned in this response |
| ↳ `hasMore` | boolean | Whether more records are available |
| ↳ `total` | number | Total number of lists matching the query |
| `success` | boolean | Operation success status |
### `hubspot_get_list`
Retrieve a single list by ID from HubSpot
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `listId` | string | Yes | The HubSpot list ID to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `list` | object | HubSpot list |
| ↳ `listId` | string | List ID |
| ↳ `name` | string | List name |
| ↳ `objectTypeId` | string | Object type ID \(e.g., 0-1 for contacts\) |
| ↳ `processingType` | string | Processing type \(MANUAL, DYNAMIC, SNAPSHOT\) |
| ↳ `processingStatus` | string | Processing status \(COMPLETE, PROCESSING\) |
| ↳ `listVersion` | number | List version number |
| ↳ `createdAt` | string | Creation date \(ISO 8601\) |
| ↳ `updatedAt` | string | Last updated date \(ISO 8601\) |
| `listId` | string | The retrieved list ID |
| `success` | boolean | Operation success status |
### `hubspot_create_list`
Create a new list in HubSpot. Specify the object type and processing type (MANUAL or DYNAMIC)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `name` | string | Yes | Name of the list |
| `objectTypeId` | string | Yes | Object type ID \(e.g., "0-1" for contacts, "0-2" for companies\) |
| `processingType` | string | Yes | Processing type: "MANUAL" for static lists or "DYNAMIC" for active lists |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `list` | object | HubSpot list |
| ↳ `listId` | string | List ID |
| ↳ `name` | string | List name |
| ↳ `objectTypeId` | string | Object type ID \(e.g., 0-1 for contacts\) |
| ↳ `processingType` | string | Processing type \(MANUAL, DYNAMIC, SNAPSHOT\) |
| ↳ `processingStatus` | string | Processing status \(COMPLETE, PROCESSING\) |
| ↳ `listVersion` | number | List version number |
| ↳ `createdAt` | string | Creation date \(ISO 8601\) |
| ↳ `updatedAt` | string | Last updated date \(ISO 8601\) |
| `listId` | string | The created list ID |
| `success` | boolean | Operation success status |

View File

@@ -0,0 +1,149 @@
---
title: Ketch
description: Manage privacy consent, subscriptions, and data subject rights
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="ketch"
color="#9B5CFF"
/>
{/* MANUAL-CONTENT-START:intro */}
[Ketch](https://www.ketch.com/) is an AI-powered privacy, consent, and data governance platform that helps organizations automate compliance with global privacy regulations. It provides tools for managing consent preferences, handling data subject rights requests, and controlling subscription communications.
With Ketch, you can:
- **Retrieve consent status**: Query the current consent preferences for any data subject across configured purposes and legal bases
- **Update consent preferences**: Set or modify consent for specific purposes (e.g., analytics, marketing) with the appropriate legal basis (opt-in, opt-out, disclosure)
- **Manage subscriptions**: Get and update subscription topic preferences and global controls across contact methods like email and SMS
- **Invoke data subject rights**: Submit privacy rights requests including data access, deletion, correction, and processing restriction under regulations like GDPR and CCPA
To use Ketch, drop the Ketch block into your workflow and provide your organization code, property code, and environment code. The Ketch Web API is a public API — no API key or OAuth credentials are required. Identity is determined by the organization and property codes along with the data subject's identity (e.g., email address).
These capabilities let you automate privacy compliance workflows, respond to user consent changes in real time, and manage data subject rights requests as part of your broader automation pipelines.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate Ketch into the workflow. Retrieve and update consent preferences, manage subscription topics and controls, and submit data subject rights requests for access, deletion, correction, or processing restriction.
## Tools
### `ketch_get_consent`
Retrieve consent status for a data subject. Returns the current consent preferences for each configured purpose.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `organizationCode` | string | Yes | Ketch organization code |
| `propertyCode` | string | Yes | Digital property code defined in Ketch |
| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) |
| `jurisdictionCode` | string | No | Jurisdiction code \(e.g., "gdpr", "ccpa"\) |
| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) |
| `purposes` | json | No | Optional purposes to filter the consent query |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `purposes` | object | Map of purpose codes to consent status and legal basis |
| ↳ `allowed` | string | Consent status for the purpose: "granted" or "denied" |
| ↳ `legalBasisCode` | string | Legal basis code \(e.g., "consent_optin", "consent_optout", "disclosure", "other"\) |
| `vendors` | object | Map of vendor consent statuses |
### `ketch_set_consent`
Update consent preferences for a data subject. Sets the consent status for specified purposes with the appropriate legal basis.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `organizationCode` | string | Yes | Ketch organization code |
| `propertyCode` | string | Yes | Digital property code defined in Ketch |
| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) |
| `jurisdictionCode` | string | No | Jurisdiction code \(e.g., "gdpr", "ccpa"\) |
| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) |
| `purposes` | json | Yes | Map of purpose codes to consent settings \(e.g., \{"analytics": \{"allowed": "granted", "legalBasisCode": "consent_optin"\}\}\) |
| `collectedAt` | number | No | UNIX timestamp when consent was collected \(defaults to current time\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `purposes` | object | Updated consent status map of purpose codes to consent settings |
| ↳ `allowed` | string | Consent status for the purpose: "granted" or "denied" |
| ↳ `legalBasisCode` | string | Legal basis code \(e.g., "consent_optin", "consent_optout", "disclosure", "other"\) |
### `ketch_get_subscriptions`
Retrieve subscription preferences for a data subject. Returns the current subscription topic and control statuses.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `organizationCode` | string | Yes | Ketch organization code |
| `propertyCode` | string | Yes | Digital property code defined in Ketch |
| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) |
| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `topics` | object | Map of topic codes to contact method settings \(e.g., \{"newsletter": \{"email": \{"status": "granted"\}\}\}\) |
| `controls` | object | Map of control codes to settings \(e.g., \{"global_unsubscribe": \{"status": "denied"\}\}\) |
### `ketch_set_subscriptions`
Update subscription preferences for a data subject. Sets topic and control statuses for email, SMS, and other contact methods.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `organizationCode` | string | Yes | Ketch organization code |
| `propertyCode` | string | Yes | Digital property code defined in Ketch |
| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) |
| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) |
| `topics` | json | No | Map of topic codes to contact method settings \(e.g., \{"newsletter": \{"email": \{"status": "granted"\}, "sms": \{"status": "denied"\}\}\}\) |
| `controls` | json | No | Map of control codes to settings \(e.g., \{"global_unsubscribe": \{"status": "denied"\}\}\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the subscription preferences were updated |
### `ketch_invoke_right`
Submit a data subject rights request (e.g., access, delete, correct, restrict processing). Initiates a privacy rights workflow in Ketch.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `organizationCode` | string | Yes | Ketch organization code |
| `propertyCode` | string | Yes | Digital property code defined in Ketch |
| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) |
| `jurisdictionCode` | string | Yes | Jurisdiction code \(e.g., "gdpr", "ccpa"\) |
| `rightCode` | string | Yes | Privacy right code to invoke \(e.g., "access", "delete", "correct", "restrict_processing"\) |
| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) |
| `userData` | json | No | Optional data subject information \(e.g., \{"email": "user@example.com", "firstName": "John", "lastName": "Doe"\}\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the rights request was submitted |
| `message` | string | Response message from Ketch |

View File

@@ -0,0 +1,388 @@
---
title: LaunchDarkly
description: Manage feature flags with LaunchDarkly.
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="launchdarkly"
color="#191919"
/>
{/* MANUAL-CONTENT-START:intro */}
[LaunchDarkly](https://launchdarkly.com/) is a feature management platform that enables teams to safely deploy, control, and measure their software features at scale.
With the LaunchDarkly integration in Sim, you can:
- **Feature flag management** — List, create, update, toggle, and delete feature flags programmatically. Toggle flags on or off in specific environments using LaunchDarkly's semantic patch API.
- **Flag status monitoring** — Check whether a flag is active, inactive, new, or launched in a given environment. Track the last time a flag was evaluated.
- **Project and environment management** — List all projects and their environments to understand your LaunchDarkly organization structure.
- **User segments** — List user segments within a project and environment to understand how your audience is organized for targeting.
- **Team visibility** — List account members and their roles for auditing and access management workflows.
- **Audit log** — Retrieve recent audit log entries to track who changed what, when. Filter entries by resource type for targeted monitoring.
In Sim, the LaunchDarkly integration enables your agents to automate feature flag operations as part of their workflows. This allows for automation scenarios such as toggling flags on/off based on deployment pipeline events, monitoring flag status and alerting on stale or unused flags, auditing flag changes by querying the audit log after deployments, syncing flag metadata with your project management tools, and listing all feature flags across projects for governance.
## Authentication
This integration uses a LaunchDarkly API key. You can create personal access tokens or service tokens in the LaunchDarkly dashboard under **Account Settings > Authorization**. The API key is passed directly in the `Authorization` header (no `Bearer` prefix).
## Need Help?
If you encounter issues with the LaunchDarkly integration, contact us at [help@sim.ai](mailto:help@sim.ai)
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate LaunchDarkly into your workflow. List, create, update, toggle, and delete feature flags. Manage projects, environments, segments, members, and audit logs. Requires API Key.
## Tools
### `launchdarkly_create_flag`
Create a new feature flag in a LaunchDarkly project.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `projectKey` | string | Yes | The project key to create the flag in |
| `name` | string | Yes | Human-readable name for the feature flag |
| `key` | string | Yes | Unique key for the feature flag \(used in code\) |
| `description` | string | No | Description of the feature flag |
| `tags` | string | No | Comma-separated list of tags |
| `temporary` | boolean | No | Whether the flag is temporary \(default true\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `key` | string | The unique key of the feature flag |
| `name` | string | The human-readable name of the feature flag |
| `kind` | string | The type of flag \(boolean or multivariate\) |
| `description` | string | Description of the feature flag |
| `temporary` | boolean | Whether the flag is temporary |
| `archived` | boolean | Whether the flag is archived |
| `deprecated` | boolean | Whether the flag is deprecated |
| `creationDate` | number | Unix timestamp in milliseconds when the flag was created |
| `tags` | array | Tags applied to the flag |
| `variations` | array | The variations for this feature flag |
| ↳ `value` | string | The variation value |
| ↳ `name` | string | The variation name |
| ↳ `description` | string | The variation description |
| `maintainerId` | string | The ID of the member who maintains this flag |
### `launchdarkly_delete_flag`
Delete a feature flag from a LaunchDarkly project.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `projectKey` | string | Yes | The project key |
| `flagKey` | string | Yes | The feature flag key to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `deleted` | boolean | Whether the flag was successfully deleted |
### `launchdarkly_get_audit_log`
List audit log entries from your LaunchDarkly account.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `limit` | number | No | Maximum number of entries to return \(default 10, max 20\) |
| `spec` | string | No | Filter expression \(e.g., "resourceType:flag"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `entries` | array | List of audit log entries |
| ↳ `id` | string | The audit log entry ID |
| ↳ `date` | number | Unix timestamp in milliseconds |
| ↳ `kind` | string | The type of action performed |
| ↳ `name` | string | The name of the resource acted on |
| ↳ `description` | string | Full description of the action |
| ↳ `shortDescription` | string | Short description of the action |
| ↳ `memberEmail` | string | Email of the member who performed the action |
| ↳ `targetName` | string | Name of the target resource |
| ↳ `targetKind` | string | Kind of the target resource |
| `totalCount` | number | Total number of audit log entries |
### `launchdarkly_get_flag`
Get a single feature flag by key from a LaunchDarkly project.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `projectKey` | string | Yes | The project key |
| `flagKey` | string | Yes | The feature flag key |
| `environmentKey` | string | No | Filter flag configuration to a specific environment |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `key` | string | The unique key of the feature flag |
| `name` | string | The human-readable name of the feature flag |
| `kind` | string | The type of flag \(boolean or multivariate\) |
| `description` | string | Description of the feature flag |
| `temporary` | boolean | Whether the flag is temporary |
| `archived` | boolean | Whether the flag is archived |
| `deprecated` | boolean | Whether the flag is deprecated |
| `creationDate` | number | Unix timestamp in milliseconds when the flag was created |
| `tags` | array | Tags applied to the flag |
| `variations` | array | The variations for this feature flag |
| ↳ `value` | string | The variation value |
| ↳ `name` | string | The variation name |
| ↳ `description` | string | The variation description |
| `maintainerId` | string | The ID of the member who maintains this flag |
| `on` | boolean | Whether the flag is on in the requested environment \(null if no single environment was specified\) |
### `launchdarkly_get_flag_status`
Get the status of a feature flag across environments (active, inactive, launched, etc.).
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `projectKey` | string | Yes | The project key |
| `flagKey` | string | Yes | The feature flag key |
| `environmentKey` | string | Yes | The environment key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `name` | string | The flag status \(new, active, inactive, launched\) |
| `lastRequested` | string | Timestamp of the last evaluation |
| `defaultVal` | string | The default variation value |
### `launchdarkly_list_environments`
List environments in a LaunchDarkly project.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `projectKey` | string | Yes | The project key to list environments for |
| `limit` | number | No | Maximum number of environments to return \(default 20\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `environments` | array | List of environments |
| ↳ `id` | string | The environment ID |
| ↳ `key` | string | The unique environment key |
| ↳ `name` | string | The environment name |
| ↳ `color` | string | The color assigned to this environment |
| ↳ `apiKey` | string | The server-side SDK key for this environment |
| ↳ `mobileKey` | string | The mobile SDK key for this environment |
| ↳ `tags` | array | Tags applied to the environment |
| `totalCount` | number | Total number of environments |
### `launchdarkly_list_flags`
List feature flags in a LaunchDarkly project.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `projectKey` | string | Yes | The project key to list flags for |
| `environmentKey` | string | No | Filter flag configurations to a specific environment |
| `tag` | string | No | Filter flags by tag name |
| `limit` | number | No | Maximum number of flags to return \(default 20\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `flags` | array | List of feature flags |
| ↳ `key` | string | The unique key of the feature flag |
| ↳ `name` | string | The human-readable name of the feature flag |
| ↳ `kind` | string | The type of flag \(boolean or multivariate\) |
| ↳ `description` | string | Description of the feature flag |
| ↳ `temporary` | boolean | Whether the flag is temporary |
| ↳ `archived` | boolean | Whether the flag is archived |
| ↳ `deprecated` | boolean | Whether the flag is deprecated |
| ↳ `creationDate` | number | Unix timestamp in milliseconds when the flag was created |
| ↳ `tags` | array | Tags applied to the flag |
| ↳ `variations` | array | The variations for this feature flag |
| ↳ `value` | string | The variation value |
| ↳ `name` | string | The variation name |
| ↳ `description` | string | The variation description |
| ↳ `maintainerId` | string | The ID of the member who maintains this flag |
| `totalCount` | number | Total number of flags |
### `launchdarkly_list_members`
List account members in your LaunchDarkly organization.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `limit` | number | No | Maximum number of members to return \(default 20\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `members` | array | List of account members |
| ↳ `id` | string | The member ID |
| ↳ `email` | string | The member email address |
| ↳ `firstName` | string | The member first name |
| ↳ `lastName` | string | The member last name |
| ↳ `role` | string | The member role \(reader, writer, admin, owner\) |
| ↳ `lastSeen` | number | Unix timestamp of last activity |
| ↳ `creationDate` | number | Unix timestamp when the member was created |
| ↳ `verified` | boolean | Whether the member email is verified |
| `totalCount` | number | Total number of members |
### `launchdarkly_list_projects`
List all projects in your LaunchDarkly account.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `limit` | number | No | Maximum number of projects to return \(default 20\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `projects` | array | List of projects |
| ↳ `id` | string | The project ID |
| ↳ `key` | string | The unique project key |
| ↳ `name` | string | The project name |
| ↳ `tags` | array | Tags applied to the project |
| `totalCount` | number | Total number of projects |
### `launchdarkly_list_segments`
List user segments in a LaunchDarkly project and environment.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `projectKey` | string | Yes | The project key |
| `environmentKey` | string | Yes | The environment key |
| `limit` | number | No | Maximum number of segments to return \(default 20\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `segments` | array | List of user segments |
| ↳ `key` | string | The unique segment key |
| ↳ `name` | string | The segment name |
| ↳ `description` | string | The segment description |
| ↳ `tags` | array | Tags applied to the segment |
| ↳ `creationDate` | number | Unix timestamp in milliseconds when the segment was created |
| ↳ `unbounded` | boolean | Whether this is an unbounded \(big\) segment |
| ↳ `included` | array | User keys explicitly included in the segment |
| ↳ `excluded` | array | User keys explicitly excluded from the segment |
| `totalCount` | number | Total number of segments |
### `launchdarkly_toggle_flag`
Toggle a feature flag on or off in a specific LaunchDarkly environment.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `projectKey` | string | Yes | The project key |
| `flagKey` | string | Yes | The feature flag key to toggle |
| `environmentKey` | string | Yes | The environment key to toggle the flag in |
| `enabled` | boolean | Yes | Whether to turn the flag on \(true\) or off \(false\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `key` | string | The unique key of the feature flag |
| `name` | string | The human-readable name of the feature flag |
| `kind` | string | The type of flag \(boolean or multivariate\) |
| `description` | string | Description of the feature flag |
| `temporary` | boolean | Whether the flag is temporary |
| `archived` | boolean | Whether the flag is archived |
| `deprecated` | boolean | Whether the flag is deprecated |
| `creationDate` | number | Unix timestamp in milliseconds when the flag was created |
| `tags` | array | Tags applied to the flag |
| `variations` | array | The variations for this feature flag |
| ↳ `value` | string | The variation value |
| ↳ `name` | string | The variation name |
| ↳ `description` | string | The variation description |
| `maintainerId` | string | The ID of the member who maintains this flag |
| `on` | boolean | Whether the flag is now on in the target environment |
### `launchdarkly_update_flag`
Update a feature flag metadata (name, description, tags, temporary, archived) using semantic patch.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `projectKey` | string | Yes | The project key |
| `flagKey` | string | Yes | The feature flag key to update |
| `updateName` | string | No | New name for the flag |
| `updateDescription` | string | No | New description for the flag |
| `addTags` | string | No | Comma-separated tags to add |
| `removeTags` | string | No | Comma-separated tags to remove |
| `archive` | boolean | No | Set to true to archive, false to restore |
| `comment` | string | No | Optional comment explaining the update |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `key` | string | The unique key of the feature flag |
| `name` | string | The human-readable name of the feature flag |
| `kind` | string | The type of flag \(boolean or multivariate\) |
| `description` | string | Description of the feature flag |
| `temporary` | boolean | Whether the flag is temporary |
| `archived` | boolean | Whether the flag is archived |
| `deprecated` | boolean | Whether the flag is deprecated |
| `creationDate` | number | Unix timestamp in milliseconds when the flag was created |
| `tags` | array | Tags applied to the flag |
| `variations` | array | The variations for this feature flag |
| ↳ `value` | string | The variation value |
| ↳ `name` | string | The variation name |
| ↳ `description` | string | The variation description |
| `maintainerId` | string | The ID of the member who maintains this flag |

View File

@@ -39,6 +39,7 @@
"enrich",
"evernote",
"exa",
"extend",
"fathom",
"file",
"firecrawl",
@@ -68,6 +69,7 @@
"google_vault",
"grafana",
"grain",
"granola",
"greenhouse",
"greptile",
"hex",
@@ -83,8 +85,10 @@
"jira",
"jira_service_management",
"kalshi",
"ketch",
"knowledge",
"langsmith",
"launchdarkly",
"lemlist",
"linear",
"linkedin",
@@ -119,17 +123,21 @@
"polymarket",
"postgresql",
"posthog",
"profound",
"pulse",
"qdrant",
"quiver",
"rds",
"reddit",
"redis",
"reducto",
"resend",
"revenuecat",
"rippling",
"s3",
"salesforce",
"search",
"secrets_manager",
"sendgrid",
"sentry",
"serper",
@@ -147,6 +155,7 @@
"stt",
"supabase",
"table",
"tailscale",
"tavily",
"telegram",
"textract",

View File

@@ -0,0 +1,626 @@
---
title: Profound
description: AI visibility and analytics with Profound
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="profound"
color="#000000"
/>
{/* MANUAL-CONTENT-START:intro */}
[Profound](https://tryprofound.com/) is an AI visibility and analytics platform that helps brands understand how they appear across AI-powered search engines, chatbots, and assistants. It tracks mentions, citations, sentiment, bot traffic, and referral patterns across platforms like ChatGPT, Perplexity, Google AI Overviews, and more.
With the Profound integration in Sim, you can:
- **Monitor AI Visibility**: Track share of voice, visibility scores, and mention counts across AI platforms for your brand and competitors.
- **Analyze Sentiment**: Measure how positively or negatively your brand is discussed in AI-generated responses.
- **Track Citations**: See which URLs are being cited by AI models and your citation share relative to competitors.
- **Monitor Bot Traffic**: Analyze AI crawler activity on your domain, including GPTBot, ClaudeBot, and other AI agents, with hourly granularity.
- **Track Referral Traffic**: Monitor human visits arriving from AI platforms to your website.
- **Explore Prompt Data**: Access raw prompt-answer pairs, query fanouts, and prompt volume trends across AI platforms.
- **Optimize Content**: Get AEO (Answer Engine Optimization) scores and actionable recommendations to improve how AI models reference your content.
- **Manage Categories & Assets**: List and explore your tracked categories, assets (brands), topics, tags, personas, and regions.
These tools let your agents automate AI visibility monitoring, competitive intelligence, and content optimization workflows. To use the Profound integration, you'll need a Profound account with API access.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Track how your brand appears across AI platforms. Monitor visibility scores, sentiment, citations, bot traffic, referrals, content optimization, and prompt volumes with Profound.
## Tools
### `profound_list_categories`
List all organization categories in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `categories` | json | List of organization categories |
| ↳ `id` | string | Category ID |
| ↳ `name` | string | Category name |
### `profound_list_regions`
List all organization regions in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `regions` | json | List of organization regions |
| ↳ `id` | string | Region ID \(UUID\) |
| ↳ `name` | string | Region name |
### `profound_list_models`
List all AI models/platforms tracked in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `models` | json | List of AI models/platforms |
| ↳ `id` | string | Model ID \(UUID\) |
| ↳ `name` | string | Model/platform name |
### `profound_list_domains`
List all organization domains in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `domains` | json | List of organization domains |
| ↳ `id` | string | Domain ID \(UUID\) |
| ↳ `name` | string | Domain name |
| ↳ `createdAt` | string | When the domain was added |
### `profound_list_assets`
List all organization assets (companies/brands) across all categories in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `assets` | json | List of organization assets with category info |
| ↳ `id` | string | Asset ID |
| ↳ `name` | string | Asset/company name |
| ↳ `website` | string | Asset website URL |
| ↳ `alternateDomains` | json | Alternate domain names |
| ↳ `isOwned` | boolean | Whether this asset is owned by the organization |
| ↳ `createdAt` | string | When the asset was created |
| ↳ `logoUrl` | string | URL of the asset logo |
| ↳ `categoryId` | string | Category ID the asset belongs to |
| ↳ `categoryName` | string | Category name |
### `profound_list_personas`
List all organization personas across all categories in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `personas` | json | List of organization personas with profile details |
| ↳ `id` | string | Persona ID |
| ↳ `name` | string | Persona name |
| ↳ `categoryId` | string | Category ID |
| ↳ `categoryName` | string | Category name |
| ↳ `persona` | json | Persona profile with behavior, employment, and demographics |
### `profound_category_topics`
List topics for a specific category in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `categoryId` | string | Yes | Category ID \(UUID\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `topics` | json | List of topics in the category |
| ↳ `id` | string | Topic ID \(UUID\) |
| ↳ `name` | string | Topic name |
### `profound_category_tags`
List tags for a specific category in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `categoryId` | string | Yes | Category ID \(UUID\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `tags` | json | List of tags in the category |
| ↳ `id` | string | Tag ID \(UUID\) |
| ↳ `name` | string | Tag name |
### `profound_category_prompts`
List prompts for a specific category in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `categoryId` | string | Yes | Category ID \(UUID\) |
| `limit` | number | No | Maximum number of results \(default 10000, max 10000\) |
| `cursor` | string | No | Pagination cursor from previous response |
| `orderDir` | string | No | Sort direction: asc or desc \(default desc\) |
| `promptType` | string | No | Comma-separated prompt types to filter: visibility, sentiment |
| `topicId` | string | No | Comma-separated topic IDs \(UUIDs\) to filter by |
| `tagId` | string | No | Comma-separated tag IDs \(UUIDs\) to filter by |
| `regionId` | string | No | Comma-separated region IDs \(UUIDs\) to filter by |
| `platformId` | string | No | Comma-separated platform IDs \(UUIDs\) to filter by |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of prompts |
| `nextCursor` | string | Cursor for next page of results |
| `prompts` | json | List of prompts |
| ↳ `id` | string | Prompt ID |
| ↳ `prompt` | string | Prompt text |
| ↳ `promptType` | string | Prompt type \(visibility or sentiment\) |
| ↳ `topicId` | string | Topic ID |
| ↳ `topicName` | string | Topic name |
| ↳ `tags` | json | Associated tags |
| ↳ `regions` | json | Associated regions |
| ↳ `platforms` | json | Associated platforms |
| ↳ `createdAt` | string | When the prompt was created |
### `profound_category_assets`
List assets (companies/brands) for a specific category in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `categoryId` | string | Yes | Category ID \(UUID\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `assets` | json | List of assets in the category |
| ↳ `id` | string | Asset ID |
| ↳ `name` | string | Asset/company name |
| ↳ `website` | string | Website URL |
| ↳ `alternateDomains` | json | Alternate domain names |
| ↳ `isOwned` | boolean | Whether the asset is owned by the organization |
| ↳ `createdAt` | string | When the asset was created |
| ↳ `logoUrl` | string | URL of the asset logo |
### `profound_category_personas`
List personas for a specific category in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `categoryId` | string | Yes | Category ID \(UUID\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `personas` | json | List of personas in the category |
| ↳ `id` | string | Persona ID |
| ↳ `name` | string | Persona name |
| ↳ `persona` | json | Persona profile with behavior, employment, and demographics |
### `profound_visibility_report`
Query AI visibility report for a category in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `categoryId` | string | Yes | Category ID \(UUID\) |
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
| `metrics` | string | Yes | Comma-separated metrics: share_of_voice, mentions_count, visibility_score, executions, average_position |
| `dimensions` | string | No | Comma-separated dimensions: date, region, topic, model, asset_name, prompt, tag, persona |
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"asset_name","operator":"is","value":"Company"\}\] |
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of rows in the report |
| `data` | json | Report data rows with metrics and dimension values |
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
### `profound_sentiment_report`
Query sentiment report for a category in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `categoryId` | string | Yes | Category ID \(UUID\) |
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
| `metrics` | string | Yes | Comma-separated metrics: positive, negative, occurrences |
| `dimensions` | string | No | Comma-separated dimensions: theme, date, region, topic, model, asset_name, tag, prompt, sentiment_type, persona |
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"asset_name","operator":"is","value":"Company"\}\] |
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of rows in the report |
| `data` | json | Report data rows with metrics and dimension values |
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
### `profound_citations_report`
Query citations report for a category in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `categoryId` | string | Yes | Category ID \(UUID\) |
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
| `metrics` | string | Yes | Comma-separated metrics: count, citation_share |
| `dimensions` | string | No | Comma-separated dimensions: hostname, path, date, region, topic, model, tag, prompt, url, root_domain, persona, citation_category |
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"hostname","operator":"is","value":"example.com"\}\] |
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of rows in the report |
| `data` | json | Report data rows with metrics and dimension values |
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
### `profound_query_fanouts`
Query fanout report showing how AI models expand prompts into sub-queries in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `categoryId` | string | Yes | Category ID \(UUID\) |
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
| `metrics` | string | Yes | Comma-separated metrics: fanouts_per_execution, total_fanouts, share |
| `dimensions` | string | No | Comma-separated dimensions: prompt, query, model, region, date |
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
| `filters` | string | No | JSON array of filter objects |
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of rows in the report |
| `data` | json | Report data rows with metrics and dimension values |
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
### `profound_prompt_answers`
Get raw prompt answers data for a category in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `categoryId` | string | Yes | Category ID \(UUID\) |
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"prompt_type","operator":"is","value":"visibility"\}\] |
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of answer rows |
| `data` | json | Raw prompt answer data |
| ↳ `prompt` | string | The prompt text |
| ↳ `promptType` | string | Prompt type \(visibility or sentiment\) |
| ↳ `response` | string | AI model response text |
| ↳ `mentions` | json | Companies/assets mentioned in the response |
| ↳ `citations` | json | URLs cited in the response |
| ↳ `topic` | string | Topic name |
| ↳ `region` | string | Region name |
| ↳ `model` | string | AI model/platform name |
| ↳ `asset` | string | Asset name |
| ↳ `createdAt` | string | Timestamp when the answer was collected |
### `profound_bots_report`
Query bot traffic report with hourly granularity for a domain in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `domain` | string | Yes | Domain to query bot traffic for \(e.g. example.com\) |
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
| `endDate` | string | No | End date \(YYYY-MM-DD or ISO 8601\). Defaults to now |
| `metrics` | string | Yes | Comma-separated metrics: count, citations, indexing, training, last_visit |
| `dimensions` | string | No | Comma-separated dimensions: date, hour, path, bot_name, bot_provider, bot_type |
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"bot_name","operator":"is","value":"GPTBot"\}\] |
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of rows in the report |
| `data` | json | Report data rows with metrics and dimension values |
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
### `profound_referrals_report`
Query human referral traffic report with hourly granularity for a domain in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `domain` | string | Yes | Domain to query referral traffic for \(e.g. example.com\) |
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
| `endDate` | string | No | End date \(YYYY-MM-DD or ISO 8601\). Defaults to now |
| `metrics` | string | Yes | Comma-separated metrics: visits, last_visit |
| `dimensions` | string | No | Comma-separated dimensions: date, hour, path, referral_source, referral_type |
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"referral_source","operator":"is","value":"openai"\}\] |
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of rows in the report |
| `data` | json | Report data rows with metrics and dimension values |
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
### `profound_raw_logs`
Get raw traffic logs with filters for a domain in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `domain` | string | Yes | Domain to query logs for \(e.g. example.com\) |
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
| `endDate` | string | No | End date \(YYYY-MM-DD or ISO 8601\). Defaults to now |
| `dimensions` | string | No | Comma-separated dimensions: timestamp, method, host, path, status_code, ip, user_agent, referer, bytes_sent, duration_ms, query_params |
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"path","operator":"contains","value":"/blog"\}\] |
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of log entries |
| `data` | json | Log data rows with metrics and dimension values |
| ↳ `metrics` | json | Array of metric values \(count\) |
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
### `profound_bot_logs`
Get identified bot visit logs with filters for a domain in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `domain` | string | Yes | Domain to query bot logs for \(e.g. example.com\) |
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
| `endDate` | string | No | End date \(YYYY-MM-DD or ISO 8601\). Defaults to now |
| `dimensions` | string | No | Comma-separated dimensions: timestamp, method, host, path, status_code, ip, user_agent, referer, bytes_sent, duration_ms, query_params, bot_name, bot_provider, bot_types |
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"bot_name","operator":"is","value":"GPTBot"\}\] |
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of bot log entries |
| `data` | json | Bot log data rows with metrics and dimension values |
| ↳ `metrics` | json | Array of metric values \(count\) |
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
### `profound_list_optimizations`
List content optimization entries for an asset in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `assetId` | string | Yes | Asset ID \(UUID\) |
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
| `offset` | number | No | Offset for pagination \(default 0\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of optimization entries |
| `optimizations` | json | List of content optimization entries |
| ↳ `id` | string | Optimization ID \(UUID\) |
| ↳ `title` | string | Content title |
| ↳ `createdAt` | string | When the optimization was created |
| ↳ `extractedInput` | string | Extracted input text |
| ↳ `type` | string | Content type: file, text, or url |
| ↳ `status` | string | Optimization status |
### `profound_optimization_analysis`
Get detailed content optimization analysis for a specific content item in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `assetId` | string | Yes | Asset ID \(UUID\) |
| `contentId` | string | Yes | Content/optimization ID \(UUID\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | json | The analyzed content |
| ↳ `format` | string | Content format: markdown or html |
| ↳ `value` | string | Content text |
| `aeoContentScore` | json | AEO content score with target zone |
| ↳ `value` | number | AEO score value |
| ↳ `targetZone` | json | Target zone range |
| ↳ `low` | number | Low end of target range |
| ↳ `high` | number | High end of target range |
| `analysis` | json | Analysis breakdown by category |
| ↳ `breakdown` | json | Array of scoring breakdowns |
| ↳ `title` | string | Category title |
| ↳ `weight` | number | Category weight |
| ↳ `score` | number | Category score |
| `recommendations` | json | Content optimization recommendations |
| ↳ `title` | string | Recommendation title |
| ↳ `status` | string | Status: done or pending |
| ↳ `impact` | json | Impact details with section and score |
| ↳ `suggestion` | json | Suggestion text and rationale |
| ↳ `text` | string | Suggestion text |
| ↳ `rationale` | string | Why this recommendation matters |
### `profound_prompt_volume`
Query prompt volume data to understand search demand across AI platforms in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
| `metrics` | string | Yes | Comma-separated metrics: volume, change |
| `dimensions` | string | No | Comma-separated dimensions: keyword, date, platform, country_code, matching_type, frequency |
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"keyword","operator":"contains","value":"best"\}\] |
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of rows in the report |
| `data` | json | Volume data rows with metrics and dimension values |
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
### `profound_citation_prompts`
Get prompts that cite a specific domain across AI platforms in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `inputDomain` | string | Yes | Domain to look up citations for \(e.g. ramp.com\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `data` | json | Citation prompt data for the queried domain |

View File

@@ -0,0 +1,132 @@
---
title: Quiver
description: Generate and vectorize SVGs
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="quiver"
color="#000000"
/>
{/* MANUAL-CONTENT-START:intro */}
[QuiverAI](https://quiver.ai/) is an AI-powered SVG generation platform that creates high-quality, scalable vector graphics from text descriptions or by vectorizing raster images. It produces clean, resolution-independent SVGs that are ideal for icons, illustrations, logos, and UI elements.
With Quiver, you can:
- **Generate SVGs from text prompts**: Describe the vector graphic you need and get production-ready SVG output
- **Vectorize raster images**: Convert PNG, JPG, and other raster images into clean SVG vector format
- **Provide reference images**: Upload up to 4 reference images to guide the style and composition of generated SVGs
- **Control generation parameters**: Adjust temperature, number of outputs, and token limits to fine-tune results
- **List available models**: Query available QuiverAI models to discover supported operations and capabilities
- **Get clean SVG markup**: Receive raw SVG content alongside downloadable files for easy embedding
In Sim, the Quiver integration enables your workflows to generate and vectorize graphics on demand. This is useful for creating dynamic illustrations, converting raster assets to scalable vectors, generating icons for applications, producing visual assets for content pipelines, or building design automation workflows. The generated SVGs are returned as files that can be passed to downstream blocks for further processing, storage, or delivery.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Generate SVG images from text prompts or vectorize raster images into SVGs using QuiverAI. Supports reference images, style instructions, and multiple output generation.
## Tools
### `quiver_text_to_svg`
Generate SVG images from text prompts using QuiverAI
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | QuiverAI API key |
| `prompt` | string | Yes | A text description of the desired SVG |
| `model` | string | Yes | The model to use for SVG generation \(e.g., "arrow-preview"\) |
| `instructions` | string | No | Style or formatting guidance for the SVG output |
| `references` | file | No | Reference images to guide SVG generation \(up to 4\) |
| `n` | number | No | Number of SVGs to generate \(1-16, default 1\) |
| `temperature` | number | No | Sampling temperature \(0-2, default 1\) |
| `top_p` | number | No | Nucleus sampling probability \(0-1, default 1\) |
| `max_output_tokens` | number | No | Maximum output tokens \(1-131072\) |
| `presence_penalty` | number | No | Token penalty for prior output \(-2 to 2, default 0\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the SVG generation succeeded |
| `output` | object | Generated SVG output |
| ↳ `file` | file | First generated SVG file |
| ↳ `files` | json | All generated SVG files \(when n &gt; 1\) |
| ↳ `svgContent` | string | Raw SVG markup content of the first result |
| ↳ `id` | string | Generation request ID |
| ↳ `usage` | json | Token usage statistics |
| ↳ `totalTokens` | number | Total tokens used |
| ↳ `inputTokens` | number | Input tokens used |
| ↳ `outputTokens` | number | Output tokens used |
### `quiver_image_to_svg`
Convert raster images into vector SVG format using QuiverAI
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | QuiverAI API key |
| `model` | string | Yes | The model to use for vectorization \(e.g., "arrow-preview"\) |
| `image` | file | Yes | The raster image to vectorize into SVG |
| `temperature` | number | No | Sampling temperature \(0-2, default 1\) |
| `top_p` | number | No | Nucleus sampling probability \(0-1, default 1\) |
| `max_output_tokens` | number | No | Maximum output tokens \(1-131072\) |
| `presence_penalty` | number | No | Token penalty for prior output \(-2 to 2, default 0\) |
| `auto_crop` | boolean | No | Automatically crop the image before vectorizing |
| `target_size` | number | No | Square resize target in pixels \(128-4096\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the vectorization succeeded |
| `output` | object | Vectorized SVG output |
| ↳ `file` | file | Generated SVG file |
| ↳ `svgContent` | string | Raw SVG markup content |
| ↳ `id` | string | Vectorization request ID |
| ↳ `usage` | json | Token usage statistics |
| ↳ `totalTokens` | number | Total tokens used |
| ↳ `inputTokens` | number | Input tokens used |
| ↳ `outputTokens` | number | Output tokens used |
### `quiver_list_models`
List all available QuiverAI models
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | QuiverAI API key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the request succeeded |
| `output` | object | Available models |
| ↳ `models` | json | List of available QuiverAI models |
| ↳ `id` | string | Model identifier |
| ↳ `name` | string | Human-readable model name |
| ↳ `description` | string | Model capabilities summary |
| ↳ `created` | number | Unix timestamp of creation |
| ↳ `ownedBy` | string | Organization that owns the model |
| ↳ `inputModalities` | json | Supported input types \(text, image, svg\) |
| ↳ `outputModalities` | json | Supported output types \(text, image, svg\) |
| ↳ `contextLength` | number | Maximum context window |
| ↳ `maxOutputLength` | number | Maximum generation length |
| ↳ `supportedOperations` | json | Available operations \(svg_generate, svg_edit, svg_animate, svg_vectorize, chat_completions\) |
| ↳ `supportedSamplingParameters` | json | Supported sampling parameters \(temperature, top_p, top_k, repetition_penalty, presence_penalty, stop\) |

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,157 @@
---
title: AWS Secrets Manager
description: Connect to AWS Secrets Manager
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="secrets_manager"
color="linear-gradient(45deg, #BD0816 0%, #FF5252 100%)"
/>
{/* MANUAL-CONTENT-START:intro */}
[AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) is a secrets management service that helps you protect access to your applications, services, and IT resources. It enables you to rotate, manage, and retrieve database credentials, API keys, and other secrets throughout their lifecycle.
With AWS Secrets Manager, you can:
- **Securely store secrets**: Encrypt secrets at rest using AWS KMS encryption keys
- **Retrieve secrets programmatically**: Access secrets from your applications and workflows without hardcoding credentials
- **Rotate secrets automatically**: Configure automatic rotation for supported services like RDS, Redshift, and DocumentDB
- **Audit access**: Track secret access and changes through AWS CloudTrail integration
- **Control access with IAM**: Use fine-grained IAM policies to manage who can access which secrets
- **Replicate across regions**: Automatically replicate secrets to multiple AWS regions for disaster recovery
In Sim, the AWS Secrets Manager integration allows your workflows to securely retrieve credentials and configuration values at runtime, create and manage secrets as part of automation pipelines, and maintain a centralized secrets store that your agents can access. This is particularly useful for workflows that need to authenticate with external services, rotate credentials, or manage sensitive configuration across environments — all without exposing secrets in your workflow definitions.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate AWS Secrets Manager into the workflow. Can retrieve, create, update, list, and delete secrets.
## Tools
### `secrets_manager_get_secret`
Retrieve a secret value from AWS Secrets Manager
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
| `accessKeyId` | string | Yes | AWS access key ID |
| `secretAccessKey` | string | Yes | AWS secret access key |
| `secretId` | string | Yes | The name or ARN of the secret to retrieve |
| `versionId` | string | No | The unique identifier of the version to retrieve |
| `versionStage` | string | No | The staging label of the version to retrieve \(e.g., AWSCURRENT, AWSPREVIOUS\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `name` | string | Name of the secret |
| `secretValue` | string | The decrypted secret value |
| `arn` | string | ARN of the secret |
| `versionId` | string | Version ID of the secret |
| `versionStages` | array | Staging labels attached to this version |
| `createdDate` | string | Date the secret was created |
### `secrets_manager_list_secrets`
List secrets stored in AWS Secrets Manager
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
| `accessKeyId` | string | Yes | AWS access key ID |
| `secretAccessKey` | string | Yes | AWS secret access key |
| `maxResults` | number | No | Maximum number of secrets to return \(1-100, default 100\) |
| `nextToken` | string | No | Pagination token from a previous request |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `secrets` | json | List of secrets with name, ARN, description, and dates |
| `nextToken` | string | Pagination token for the next page of results |
| `count` | number | Number of secrets returned |
### `secrets_manager_create_secret`
Create a new secret in AWS Secrets Manager
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
| `accessKeyId` | string | Yes | AWS access key ID |
| `secretAccessKey` | string | Yes | AWS secret access key |
| `name` | string | Yes | Name of the secret to create |
| `secretValue` | string | Yes | The secret value \(plain text or JSON string\) |
| `description` | string | No | Description of the secret |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
| `name` | string | Name of the created secret |
| `arn` | string | ARN of the created secret |
| `versionId` | string | Version ID of the created secret |
### `secrets_manager_update_secret`
Update the value of an existing secret in AWS Secrets Manager
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
| `accessKeyId` | string | Yes | AWS access key ID |
| `secretAccessKey` | string | Yes | AWS secret access key |
| `secretId` | string | Yes | The name or ARN of the secret to update |
| `secretValue` | string | Yes | The new secret value \(plain text or JSON string\) |
| `description` | string | No | Updated description of the secret |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
| `name` | string | Name of the updated secret |
| `arn` | string | ARN of the updated secret |
| `versionId` | string | Version ID of the updated secret |
### `secrets_manager_delete_secret`
Delete a secret from AWS Secrets Manager
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
| `accessKeyId` | string | Yes | AWS access key ID |
| `secretAccessKey` | string | Yes | AWS secret access key |
| `secretId` | string | Yes | The name or ARN of the secret to delete |
| `recoveryWindowInDays` | number | No | Number of days before permanent deletion \(7-30, default 30\) |
| `forceDelete` | boolean | No | If true, immediately delete without recovery window |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
| `name` | string | Name of the deleted secret |
| `arn` | string | ARN of the deleted secret |
| `deletionDate` | string | Scheduled deletion date |

View File

@@ -925,6 +925,82 @@ Create a canvas pinned to a Slack channel as its resource hub
| --------- | ---- | ----------- |
| `canvas_id` | string | ID of the created channel canvas |
### `slack_create_conversation`
Create a new public or private channel in a Slack workspace.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | No | Authentication method: oauth or bot_token |
| `botToken` | string | No | Bot token for Custom Bot |
| `name` | string | Yes | Name of the channel to create \(lowercase, numbers, hyphens, underscores only; max 80 characters\) |
| `isPrivate` | boolean | No | Create a private channel instead of a public one \(default: false\) |
| `teamId` | string | No | Encoded team ID to create the channel in \(required if using an org token\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `channelInfo` | object | The newly created channel object |
| ↳ `id` | string | Channel ID \(e.g., C1234567890\) |
| ↳ `name` | string | Channel name without # prefix |
| ↳ `is_channel` | boolean | Whether this is a channel |
| ↳ `is_private` | boolean | Whether channel is private |
| ↳ `is_archived` | boolean | Whether channel is archived |
| ↳ `is_general` | boolean | Whether this is the general channel |
| ↳ `is_member` | boolean | Whether the bot/user is a member |
| ↳ `is_shared` | boolean | Whether channel is shared across workspaces |
| ↳ `is_ext_shared` | boolean | Whether channel is externally shared |
| ↳ `is_org_shared` | boolean | Whether channel is org-wide shared |
| ↳ `num_members` | number | Number of members in the channel |
| ↳ `topic` | string | Channel topic |
| ↳ `purpose` | string | Channel purpose/description |
| ↳ `created` | number | Unix timestamp when channel was created |
| ↳ `creator` | string | User ID of channel creator |
| ↳ `updated` | number | Unix timestamp of last update |
### `slack_invite_to_conversation`
Invite one or more users to a Slack channel. Supports up to 100 users at a time.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | No | Authentication method: oauth or bot_token |
| `botToken` | string | No | Bot token for Custom Bot |
| `channel` | string | Yes | The ID of the channel to invite users to |
| `users` | string | Yes | Comma-separated list of user IDs to invite \(up to 100\) |
| `force` | boolean | No | When true, continues inviting valid users while skipping invalid ones \(default: false\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `channelInfo` | object | The channel object after inviting users |
| ↳ `id` | string | Channel ID \(e.g., C1234567890\) |
| ↳ `name` | string | Channel name without # prefix |
| ↳ `is_channel` | boolean | Whether this is a channel |
| ↳ `is_private` | boolean | Whether channel is private |
| ↳ `is_archived` | boolean | Whether channel is archived |
| ↳ `is_general` | boolean | Whether this is the general channel |
| ↳ `is_member` | boolean | Whether the bot/user is a member |
| ↳ `is_shared` | boolean | Whether channel is shared across workspaces |
| ↳ `is_ext_shared` | boolean | Whether channel is externally shared |
| ↳ `is_org_shared` | boolean | Whether channel is org-wide shared |
| ↳ `num_members` | number | Number of members in the channel |
| ↳ `topic` | string | Channel topic |
| ↳ `purpose` | string | Channel purpose/description |
| ↳ `created` | number | Unix timestamp when channel was created |
| ↳ `creator` | string | User ID of channel creator |
| ↳ `updated` | number | Unix timestamp of last update |
| `errors` | array | Per-user errors when force is true and some invitations failed |
| ↳ `user` | string | User ID that failed |
| ↳ `ok` | boolean | Always false for error entries |
| ↳ `error` | string | Error code for this user |
### `slack_open_view`
Open a modal view in Slack using a trigger_id from an interaction payload. Used to display forms, confirmations, and other interactive modals.

View File

@@ -0,0 +1,498 @@
---
title: Tailscale
description: Manage devices and network settings in your Tailscale tailnet
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="tailscale"
color="#2E2D2D"
/>
{/* MANUAL-CONTENT-START:intro */}
## Overview
[Tailscale](https://tailscale.com) is a zero-config mesh VPN built on WireGuard that makes it easy to connect devices, services, and users across any network. The Tailscale block lets you automate network management tasks like device provisioning, access control, route management, and DNS configuration directly from your Sim workflows.
## Authentication
The Tailscale block uses API key authentication. To get an API key:
1. Go to the [Tailscale admin console](https://login.tailscale.com/admin/settings/keys)
2. Navigate to **Settings > Keys**
3. Click **Generate API key**
4. Set an expiry (1-90 days) and copy the key (starts with `tskey-api-`)
You must have an **Owner**, **Admin**, **IT admin**, or **Network admin** role to generate API keys.
## Tailnet Identifier
Every operation requires a **tailnet** parameter. This is typically your organization's domain name (e.g., `example.com`). You can also use `"-"` to refer to your default tailnet.
## Common Use Cases
- **Device inventory**: List and monitor all devices connected to your network
- **Automated provisioning**: Create and manage auth keys to pre-authorize new devices
- **Access control**: Authorize or deauthorize devices, manage device tags for ACL policies
- **Route management**: View and enable subnet routes for devices acting as subnet routers
- **DNS management**: Configure nameservers, MagicDNS, and search paths
- **Key lifecycle**: Create, list, inspect, and revoke auth keys
- **User auditing**: List all users in the tailnet and their roles
- **Policy review**: Retrieve the current ACL policy for inspection or backup
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Interact with the Tailscale API to manage devices, DNS, ACLs, auth keys, users, and routes across your tailnet.
## Tools
### `tailscale_list_devices`
List all devices in the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `devices` | array | List of devices in the tailnet |
| ↳ `id` | string | Device ID |
| ↳ `name` | string | Device name |
| ↳ `hostname` | string | Device hostname |
| ↳ `user` | string | Associated user |
| ↳ `os` | string | Operating system |
| ↳ `clientVersion` | string | Tailscale client version |
| ↳ `addresses` | array | Tailscale IP addresses |
| ↳ `tags` | array | Device tags |
| ↳ `authorized` | boolean | Whether the device is authorized |
| ↳ `blocksIncomingConnections` | boolean | Whether the device blocks incoming connections |
| ↳ `lastSeen` | string | Last seen timestamp |
| ↳ `created` | string | Creation timestamp |
| `count` | number | Total number of devices |
### `tailscale_get_device`
Get details of a specific device by ID
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `deviceId` | string | Yes | Device ID |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Device ID |
| `name` | string | Device name |
| `hostname` | string | Device hostname |
| `user` | string | Associated user |
| `os` | string | Operating system |
| `clientVersion` | string | Tailscale client version |
| `addresses` | array | Tailscale IP addresses |
| `tags` | array | Device tags |
| `authorized` | boolean | Whether the device is authorized |
| `blocksIncomingConnections` | boolean | Whether the device blocks incoming connections |
| `lastSeen` | string | Last seen timestamp |
| `created` | string | Creation timestamp |
| `isExternal` | boolean | Whether the device is external |
| `updateAvailable` | boolean | Whether an update is available |
| `machineKey` | string | Machine key |
| `nodeKey` | string | Node key |
### `tailscale_delete_device`
Remove a device from the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `deviceId` | string | Yes | Device ID to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the device was successfully deleted |
| `deviceId` | string | ID of the deleted device |
### `tailscale_authorize_device`
Authorize or deauthorize a device on the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `deviceId` | string | Yes | Device ID to authorize |
| `authorized` | boolean | Yes | Whether to authorize \(true\) or deauthorize \(false\) the device |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the operation succeeded |
| `deviceId` | string | Device ID |
| `authorized` | boolean | Authorization status after the operation |
### `tailscale_set_device_tags`
Set tags on a device in the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `deviceId` | string | Yes | Device ID |
| `tags` | string | Yes | Comma-separated list of tags \(e.g., "tag:server,tag:production"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the tags were successfully set |
| `deviceId` | string | Device ID |
| `tags` | array | Tags set on the device |
### `tailscale_get_device_routes`
Get the subnet routes for a device
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `deviceId` | string | Yes | Device ID |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `advertisedRoutes` | array | Subnet routes the device is advertising |
| `enabledRoutes` | array | Subnet routes that are approved/enabled |
### `tailscale_set_device_routes`
Set the enabled subnet routes for a device
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `deviceId` | string | Yes | Device ID |
| `routes` | string | Yes | Comma-separated list of subnet routes to enable \(e.g., "10.0.0.0/24,192.168.1.0/24"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `advertisedRoutes` | array | Subnet routes the device is advertising |
| `enabledRoutes` | array | Subnet routes that are now enabled |
### `tailscale_update_device_key`
Enable or disable key expiry on a device
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `deviceId` | string | Yes | Device ID |
| `keyExpiryDisabled` | boolean | Yes | Whether to disable key expiry \(true\) or enable it \(false\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the operation succeeded |
| `deviceId` | string | Device ID |
| `keyExpiryDisabled` | boolean | Whether key expiry is now disabled |
### `tailscale_list_dns_nameservers`
Get the DNS nameservers configured for the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `dns` | array | List of DNS nameserver addresses |
| `magicDNS` | boolean | Whether MagicDNS is enabled |
### `tailscale_set_dns_nameservers`
Set the DNS nameservers for the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `dns` | string | Yes | Comma-separated list of DNS nameserver IP addresses \(e.g., "8.8.8.8,8.8.4.4"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `dns` | array | Updated list of DNS nameserver addresses |
| `magicDNS` | boolean | Whether MagicDNS is enabled |
### `tailscale_get_dns_preferences`
Get the DNS preferences for the tailnet including MagicDNS status
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `magicDNS` | boolean | Whether MagicDNS is enabled |
### `tailscale_set_dns_preferences`
Set DNS preferences for the tailnet (enable/disable MagicDNS)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `magicDNS` | boolean | Yes | Whether to enable \(true\) or disable \(false\) MagicDNS |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `magicDNS` | boolean | Updated MagicDNS status |
### `tailscale_get_dns_searchpaths`
Get the DNS search paths configured for the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `searchPaths` | array | List of DNS search path domains |
### `tailscale_set_dns_searchpaths`
Set the DNS search paths for the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `searchPaths` | string | Yes | Comma-separated list of DNS search path domains \(e.g., "corp.example.com,internal.example.com"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `searchPaths` | array | Updated list of DNS search path domains |
### `tailscale_list_users`
List all users in the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `users` | array | List of users in the tailnet |
| ↳ `id` | string | User ID |
| ↳ `displayName` | string | Display name |
| ↳ `loginName` | string | Login name / email |
| ↳ `profilePicURL` | string | Profile picture URL |
| ↳ `role` | string | User role \(owner, admin, member, etc.\) |
| ↳ `status` | string | User status \(active, suspended, etc.\) |
| ↳ `type` | string | User type \(member, shared, tagged\) |
| ↳ `created` | string | Creation timestamp |
| ↳ `lastSeen` | string | Last seen timestamp |
| ↳ `deviceCount` | number | Number of devices owned by user |
| `count` | number | Total number of users |
### `tailscale_create_auth_key`
Create a new auth key for the tailnet to pre-authorize devices
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `reusable` | boolean | No | Whether the key can be used more than once |
| `ephemeral` | boolean | No | Whether devices authenticated with this key are ephemeral |
| `preauthorized` | boolean | No | Whether devices are pre-authorized \(skip manual approval\) |
| `tags` | string | No | Comma-separated list of tags for devices using this key \(e.g., "tag:server,tag:prod"\) |
| `description` | string | No | Description for the auth key |
| `expirySeconds` | number | No | Key expiry time in seconds \(default: 90 days\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Auth key ID |
| `key` | string | The auth key value \(only shown once at creation\) |
| `description` | string | Key description |
| `created` | string | Creation timestamp |
| `expires` | string | Expiration timestamp |
| `revoked` | string | Revocation timestamp \(empty if not revoked\) |
| `capabilities` | object | Key capabilities |
| ↳ `reusable` | boolean | Whether the key is reusable |
| ↳ `ephemeral` | boolean | Whether devices are ephemeral |
| ↳ `preauthorized` | boolean | Whether devices are pre-authorized |
| ↳ `tags` | array | Tags applied to devices using this key |
### `tailscale_list_auth_keys`
List all auth keys in the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `keys` | array | List of auth keys |
| ↳ `id` | string | Auth key ID |
| ↳ `description` | string | Key description |
| ↳ `created` | string | Creation timestamp |
| ↳ `expires` | string | Expiration timestamp |
| ↳ `revoked` | string | Revocation timestamp |
| ↳ `capabilities` | object | Key capabilities |
| ↳ `reusable` | boolean | Whether the key is reusable |
| ↳ `ephemeral` | boolean | Whether devices are ephemeral |
| ↳ `preauthorized` | boolean | Whether devices are pre-authorized |
| ↳ `tags` | array | Tags applied to devices |
| `count` | number | Total number of auth keys |
### `tailscale_get_auth_key`
Get details of a specific auth key
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `keyId` | string | Yes | Auth key ID |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Auth key ID |
| `description` | string | Key description |
| `created` | string | Creation timestamp |
| `expires` | string | Expiration timestamp |
| `revoked` | string | Revocation timestamp |
| `capabilities` | object | Key capabilities |
| ↳ `reusable` | boolean | Whether the key is reusable |
| ↳ `ephemeral` | boolean | Whether devices are ephemeral |
| ↳ `preauthorized` | boolean | Whether devices are pre-authorized |
| ↳ `tags` | array | Tags applied to devices using this key |
### `tailscale_delete_auth_key`
Revoke and delete an auth key
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `keyId` | string | Yes | Auth key ID to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the auth key was successfully deleted |
| `keyId` | string | ID of the deleted auth key |
### `tailscale_get_acl`
Get the current ACL policy for the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `acl` | string | ACL policy as JSON string |
| `etag` | string | ETag for the current ACL version \(use with If-Match header for updates\) |

View File

@@ -131,7 +131,7 @@ Detecta información de identificación personal utilizando Microsoft Presidio.
**Casos de uso:**
- Bloquear contenido que contiene información personal sensible
- Enmascarar PII antes de registrar o almacenar datos
- Cumplimiento de GDPR, HIPAA y otras regulaciones de privacidad
- Cumplimiento de GDPR y otras regulaciones de privacidad
- Sanear entradas de usuario antes del procesamiento
## Configuración

View File

@@ -131,7 +131,7 @@ Détecte les informations personnelles identifiables à l'aide de Microsoft Pres
**Cas d'utilisation :**
- Bloquer le contenu contenant des informations personnelles sensibles
- Masquer les PII avant de journaliser ou stocker des données
- Conformité avec le RGPD, HIPAA et autres réglementations sur la confidentialité
- Conformité avec le RGPD et autres réglementations sur la confidentialité
- Assainir les entrées utilisateur avant traitement
## Configuration

View File

@@ -131,7 +131,7 @@ Microsoft Presidioを使用して個人を特定できる情報を検出しま
**ユースケース:**
- 機密性の高い個人情報を含むコンテンツをブロック
- データのログ記録や保存前にPIIをマスク
- GDPR、HIPAA、その他のプライバシー規制への準拠
- GDPR、その他のプライバシー規制への準拠
- 処理前のユーザー入力のサニタイズ
## 設定

View File

@@ -131,7 +131,7 @@ Guardrails 模块通过针对多种验证类型检查内容,验证并保护您
**使用场景:**
- 阻止包含敏感个人信息的内容
- 在记录或存储数据之前屏蔽 PII
- 符合 GDPR、HIPAA 和其他隐私法规
- 符合 GDPR 和其他隐私法规
- 在处理之前清理用户输入
## 配置

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 521 B

After

Width:  |  Height:  |  Size: 425 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 814 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,14 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<rect width="16" height="16" rx="3" fill="#0B0B0B"/>
<g transform="translate(2.75,2.75) scale(0.0473)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M107.822 93.7612C107.822 97.3481 106.403 100.792 103.884 103.328L103.523 103.692C101.006 106.236 97.5855 107.658 94.0236 107.658H13.4455C6.02456 107.658 0 113.718 0 121.191V208.332C0 215.806 6.02456 221.866 13.4455 221.866H99.9622C107.383 221.866 113.4 215.806 113.4 208.332V126.745C113.4 123.419 114.71 120.228 117.047 117.874C119.377 115.527 122.546 114.207 125.849 114.207H207.777C215.198 114.207 221.214 108.148 221.214 100.674V13.5333C221.214 6.05956 215.198 0 207.777 0H121.26C113.839 0 107.822 6.05956 107.822 13.5333V93.7612ZM134.078 18.55H194.952C199.289 18.55 202.796 22.0893 202.796 26.4503V87.7574C202.796 92.1178 199.289 95.6577 194.952 95.6577H134.078C129.748 95.6577 126.233 92.1178 126.233 87.7574V26.4503C126.233 22.0893 129.748 18.55 134.078 18.55Z" fill="#33C482"/>
<path d="M207.878 129.57H143.554C135.756 129.57 129.434 135.937 129.434 143.791V207.784C129.434 215.638 135.756 222.005 143.554 222.005H207.878C215.677 222.005 221.999 215.638 221.999 207.784V143.791C221.999 135.937 215.677 129.57 207.878 129.57Z" fill="#33C482"/>
<path d="M207.878 129.266H143.554C135.756 129.266 129.434 135.632 129.434 143.487V207.479C129.434 215.333 135.756 221.699 143.554 221.699H207.878C215.677 221.699 221.999 215.333 221.999 207.479V143.487C221.999 135.632 215.677 129.266 207.878 129.266Z" fill="url(#paint0_linear)" fill-opacity="0.2"/>
</g>
<defs>
<linearGradient id="paint0_linear" x1="129.434" y1="129.266" x2="185.629" y2="185.33" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="129.434" x2="185.629" y1="129.266" y2="185.33"><stop offset="0"/><stop offset="1" stop-opacity="0"/></linearGradient><rect fill="#0b0b0b" height="16" rx="3" width="16"/><g transform="matrix(.0473 0 0 .0473 2.75 2.75)"><path clip-rule="evenodd" d="m107.822 93.7612c0 3.5869-1.419 7.0308-3.938 9.5668l-.361.364c-2.517 2.544-5.9375 3.966-9.4994 3.966h-80.5781c-7.42094 0-13.4455 6.06-13.4455 13.533v87.141c0 7.474 6.02456 13.534 13.4455 13.534h86.5167c7.4208 0 13.4378-6.06 13.4378-13.534v-81.587c0-3.326 1.31-6.517 3.647-8.871 2.33-2.347 5.499-3.667 8.802-3.667h81.928c7.421 0 13.437-6.059 13.437-13.533v-87.1407c0-7.47374-6.016-13.5333-13.437-13.5333h-86.517c-7.421 0-13.438 6.05956-13.438 13.5333zm26.256-75.2112h60.874c4.337 0 7.844 3.5393 7.844 7.9003v61.3071c0 4.3604-3.507 7.9003-7.844 7.9003h-60.874c-4.33 0-7.845-3.5399-7.845-7.9003v-61.3071c0-4.361 3.515-7.9003 7.845-7.9003z" fill="#33c482" fill-rule="evenodd"/><path d="m207.878 129.57h-64.324c-7.798 0-14.12 6.367-14.12 14.221v63.993c0 7.854 6.322 14.221 14.12 14.221h64.324c7.799 0 14.121-6.367 14.121-14.221v-63.993c0-7.854-6.322-14.221-14.121-14.221z" fill="#33c482"/><path d="m207.878 129.266h-64.324c-7.798 0-14.12 6.366-14.12 14.221v63.992c0 7.854 6.322 14.22 14.12 14.22h64.324c7.799 0 14.121-6.366 14.121-14.22v-63.992c0-7.855-6.322-14.221-14.121-14.221z" fill="url(#a)" fill-opacity=".2"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Some files were not shown because too many files have changed in this diff Show More