Compare commits

..

261 Commits

Author SHA1 Message Date
Theodore Li
efb23dad5e Make migration concurrent 2026-03-10 00:18:58 -07:00
Theodore Li
11f402b031 Personal key should only be used if its the owner of the workflow 2026-03-09 21:30:01 -07:00
Theodore Li
bba16f2032 Prefer plain > workspace > personal 2026-03-09 21:22:35 -07:00
Theodore Li
e8ac2736a3 Sleep only between workspaces, not during single workspace 2026-03-09 21:22:35 -07:00
Theodore Li
e127817436 Fix lint 2026-03-09 21:22:35 -07:00
Theodore Li
a2329e2a2a Add sleep every 1000, dry run writes to files that prod consumes from 2026-03-09 21:22:35 -07:00
Theodore Li
9a1b68120c Fix row count bug 2026-03-09 21:22:34 -07:00
Theodore Li
6e7bdaedda Refactor to iterate per workspace to avoid overconsuming memory 2026-03-09 21:22:34 -07:00
Theodore Li
01b21ed004 Change script to iterate on workflow 2026-03-09 21:22:34 -07:00
Theodore Li
70e76e8030 Add byok migration script for newly hosted keys 2026-03-09 21:22:34 -07:00
waleed
5c6797a0bd revert hardcoded ff 2026-03-09 21:09:29 -07:00
waleed
57f5f6e59a improvement(sidebar): match workspace switcher popover width to sidebar
Use Radix UI's built-in --radix-popover-trigger-width CSS variable
instead of hardcoded 160px so the popover matches the trigger width
and responds to sidebar resizing.
2026-03-09 21:08:24 -07:00
waleed
f26a375f3c chore: lint fixes 2026-03-09 20:59:46 -07:00
waleed
1cb8a28727 fix(settings): align skeleton layouts with actual component structures
- Fix list item gap from 12px to 8px across all skeletons (API keys, custom tools, credentials, MCP)
- Add OAuth icon placeholder to credential skeleton
- Fix credential button group gap from 8px to 4px
- Remove incorrect gap-[4px] from credential-sets text column
- Rebuild debug skeleton to match real layout (description + input/button row)
- Add scrollable wrapper to BYOK skeleton with more representative item count
2026-03-09 20:59:18 -07:00
waleed
f62fddfac5 improvement(settings): add search bar to skeleton loading states
Skeletons now include the search bar (and action button where applicable) so the layout matches the final component 1:1. Eliminates layout shift when the dynamic chunk loads — search bar area is already reserved by the skeleton.
2026-03-09 20:45:35 -07:00
waleed
5184580dbd Merge branch 'improvement/settings-perf' into feat/mothership-copilot 2026-03-09 20:35:39 -07:00
waleed
1aa9dc9ea7 fix(settings): use emcn Input for file input in general settings 2026-03-09 20:34:52 -07:00
waleed
37a1d66127 fix(byok): use ui Input for search bar to match other settings pages 2026-03-09 20:33:03 -07:00
waleed
5a5bf5ca7e fix(byok): use EMCN Input for search field instead of ui Input
Replace @/components/ui Input with the already-imported EmcnInput for design-system consistency.
2026-03-09 20:29:45 -07:00
waleed
4ccc1e5997 fix(settings): include theme sync in client-side prefetch queryFn
Hover-based prefetchGeneralSettings now calls syncThemeToNextThemes, matching the useGeneralSettings hook behavior so theme updates aren't missed when prefetch refreshes stale cache.
2026-03-09 20:29:06 -07:00
waleed
80f032b9be update byok page 2026-03-09 20:22:45 -07:00
waleed
63927e5afc fix(settings): extract shared response mappers to prevent server/client shape drift
Addresses PR review feedback — prefetch.ts duplicated response mapping logic from client hooks. Extracted mapGeneralSettingsResponse and mapUserProfileResponse as shared functions used by both client fetch and server prefetch.
2026-03-09 20:21:43 -07:00
waleed
ab61f5188c fix(settings): use emcn Skeleton in extracted skeleton files 2026-03-09 20:09:19 -07:00
waleed
94914b848e fix: bust browser cache for workspace file downloads
The downloadFile function was using a plain fetch() that honored the
aggressive cache headers, causing newly created files to download empty.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:07:27 -07:00
waleed
4c7e63cf7a improvement(settings): SSR prefetch, code splitting, dedicated skeletons 2026-03-09 20:01:29 -07:00
Theodore Li
8fc75a6e9d feat(hosted-key-services) Add hosted key for multiple services (#3461)
* feat(hosted keys): Implement serper hosted key

* Handle required fields correctly for hosted keys

* Add rate limiting (3 tries, exponential backoff)

* Add custom pricing, switch to exa as first hosted key

* Add telemetry

* Consolidate byok type definitions

* Add warning comment if default calculation is used

* Record usage to user stats table

* Fix unit tests, use cost property

* Include more metadata in cost output

* Fix disabled tests

* Fix spacing

* Fix lint

* Move knowledge cost restructuring away from generic block handler

* Migrate knowledge unit tests

* Lint

* Fix broken tests

* Add user based hosted key throttling

* Refactor hosted key handling. Add optimistic handling of throttling for custom throttle rules.

* Remove research as hosted key. Recommend BYOK if throtttling occurs

* Make adding api keys adjustable via env vars

* Remove vestigial fields from research

* Make billing actor id required for throttling

* Switch to round robin for api key distribution

* Add helper method for adding hosted key cost

* Strip leading double underscores to avoid breaking change

* Lint fix

* Remove falsy check in favor for explicit null check

* Add more detailed metrics for different throttling types

* Fix _costDollars field

* Handle hosted agent tool calls

* Fail loudly if cost field isn't found

* Remove any type

* Fix type error

* Fix lint

* Fix usage log double logging data

* Fix test

* Add browseruse hosted key

* Add firecrawl and serper hosted keys

* feat(hosted key): Add exa hosted key (#3221)

* feat(hosted keys): Implement serper hosted key

* Handle required fields correctly for hosted keys

* Add rate limiting (3 tries, exponential backoff)

* Add custom pricing, switch to exa as first hosted key

* Add telemetry

* Consolidate byok type definitions

* Add warning comment if default calculation is used

* Record usage to user stats table

* Fix unit tests, use cost property

* Include more metadata in cost output

* Fix disabled tests

* Fix spacing

* Fix lint

* Move knowledge cost restructuring away from generic block handler

* Migrate knowledge unit tests

* Lint

* Fix broken tests

* Add user based hosted key throttling

* Refactor hosted key handling. Add optimistic handling of throttling for custom throttle rules.

* Remove research as hosted key. Recommend BYOK if throtttling occurs

* Make adding api keys adjustable via env vars

* Remove vestigial fields from research

* Make billing actor id required for throttling

* Switch to round robin for api key distribution

* Add helper method for adding hosted key cost

* Strip leading double underscores to avoid breaking change

* Lint fix

* Remove falsy check in favor for explicit null check

* Add more detailed metrics for different throttling types

* Fix _costDollars field

* Handle hosted agent tool calls

* Fail loudly if cost field isn't found

* Remove any type

* Fix type error

* Fix lint

* Fix usage log double logging data

* Fix test

---------

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

* Fail fast on cost data not being found

* Add hosted key for google services

* Add hosting configuration and pricing logic for ElevenLabs TTS tools

* Add linkup hosted key

* Add jina hosted key

* Add hugging face hosted key

* Add perplexity hosting

* Add broader metrics for throttling

* Add skill for adding hosted key

* Lint, remove vestigial hosted keys not implemented

* Revert agent changes

* fail fast

* Fix build issue

* Fix build issues

* Fix type error

* Remove byok types that aren't implemented

* Address feedback

* Use default model when model id isn't provided

* Fix cost default issues

* Remove firecrawl error suppression

* Restore original behavior for hugging face

* Add mistral hosted key

* Remove hugging face hosted key

* Fix pricing mismatch is mistral and perplexity

* Add hosted keys for parallel and brand fetch

* Add brandfetch hosted key

* Update types

* Change byok name to parallel_ai

* Add telemetry on unknown models

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-03-09 22:56:45 -04:00
Vikhyath Mondreti
9400df6085 Merge branch 'feat/mothership-copilot' of github.com:simstudioai/sim into feat/mothership-copilot 2026-03-09 19:23:06 -07:00
Vikhyath Mondreti
d23afb97c5 fix(credentials): block usage at execution layer without perms + fix invites 2026-03-09 19:22:35 -07:00
Siddharth Ganesan
1ff89cd416 Table batch ops 2026-03-09 19:04:30 -07:00
waleed
b7a6fe574c small table rename bug, files updates not persisting 2026-03-09 18:38:40 -07:00
Siddharth Ganesan
8abe717b85 Fix table column delete 2026-03-09 18:25:07 -07:00
Emir Karabeg
d815568315 improvement: tables, chat 2026-03-09 18:23:52 -07:00
waleed
5dc026c72e upgrade turbo 2026-03-09 18:22:20 -07:00
waleed
86b67823ce update docs 2026-03-09 18:21:53 -07:00
Vikhyath Mondreti
65448766fc Merge branch 'feat/mothership-copilot' of github.com:simstudioai/sim into feat/mothership-copilot 2026-03-09 18:03:49 -07:00
Vikhyath Mondreti
9098f0b805 fix(credentials): exclude regular login methods from credential sync 2026-03-09 18:03:25 -07:00
waleed
78e3d840dd updated document icon 2026-03-09 18:00:26 -07:00
waleed
09af6fb33d improve resizer for file preview for html files 2026-03-09 17:48:01 -07:00
Emir Karabeg
523aff8ab0 improvements(tables): styling improvements 2026-03-09 17:46:27 -07:00
Emir Karabeg
4fe9509e70 styling alignment 2026-03-09 17:46:27 -07:00
Emir Karabeg
9a1cb10d7a improvement(ui): consistent styling 2026-03-09 17:46:27 -07:00
Theodore Li
898f8ce1c1 feat(exa-hosted-key): Restore exa hosted key (#3499)
Co-authored-by: Theodore Li <theo@sim.ai>
2026-03-09 20:40:54 -04:00
Waleed
39334bdf7d fix(tables): one small tables ting (#3497) 2026-03-09 16:43:15 -07:00
Waleed
a6d3b3a9ad improvement(tables): click-to-select navigation, inline rename, column resize (#3496)
* improvement(tables): click-to-select navigation, inline rename, column resize

* fix(tables): address PR review comments

- Add doneRef guard to useInlineRename preventing Enter+blur double-fire
- Fix PATCH error handler: return 500 for non-validation errors, fix unreachable logger.error
- Stop click propagation on breadcrumb rename input

* fix(tables): add rows-affected check in renameTable service

Prevents silent no-op when tableId doesn't match any record.

* fix(tables): useMemo deps + placeholder memo initialCharacter check

- Use primitive editingId/editValue in useMemo deps instead of whole
  useInlineRename object (which creates a new ref every render)
- Add initialCharacter comparison to placeholderPropsAreEqual, matching
  the existing pattern in dataRowPropsAreEqual

* fix(tables): address round 2 review comments

- Mirror name validation (regex + max length) in PatchTableSchema so
  validateTableName failures return 400 instead of 500
- Add .returning() + rows-affected check to renameWorkspaceFile,
  matching the renameTable pattern
- Check response.ok before parsing JSON in useRenameWorkspaceFile,
  matching the useRenameTable pattern

* refactor(tables): reuse InlineRenameInput in BreadcrumbSegment

Replace duplicated inline input markup with the shared component.
Eliminates redundant useRef, useEffect, and input boilerplate.

* fix(tables): set doneRef in cancelRename to prevent blur-triggered save

Escape → cancelRename → input unmounts → blur → submitRename would
save instead of canceling. Now cancelRename sets doneRef like
submitRename does, blocking the subsequent blur handler.

* fix(tables): pointercancel cleanup + typed FileConflictError

- Add pointercancel handler to column resize to prevent listener leaks
  when system interrupts the pointer (touch-action override, etc.)
- Replace stringly-typed error.message.includes('already exists') with
  FileConflictError class for refactor-safe 409 status detection

* fix(tables): stable useCallback dep + rename shadowed variable

- Use listRename.startRename (stable ref) instead of whole listRename
  object in handleContextMenuRename deps
- Rename inner 'target' to 'origin' in arrow-key handler to avoid
  shadowing the outer HTMLElement 'target'

* fix(tables): move class below imports, stable submitRename, clear editingCell

- Move FileConflictError below import statements (import-first convention)
- Make submitRename a stable useCallback([]) by reading editingId and
  editValue through refs (matches existing onSaveRef pattern)
- Add setEditingCell(null) to handleEmptyRowClick for symmetry with
  handleCellClick

* feat(tables): persist column widths in table metadata

Column widths now survive navigation and page reloads. On resize-end,
widths are debounced (500ms) and saved to the table's metadata field
via a new PUT /api/table/[tableId]/metadata endpoint. On load, widths
are seeded from the server once via React Query.

* fix type checking for file viewer

* fix(tables): address review feedback — 4 fixes

1. headerRename.onSave now uses the fileId parameter directly instead
   of the selectedFile closure, preventing rename-wrong-file race
2. updateMetadataMutation uses ref pattern matching mutateRef/createRef
3. Type-to-enter filters non-numeric chars for number columns, non-date
   chars for date columns
4. renameValue only passed to actively-renaming ColumnHeaderMenu,
   preserving React.memo for other columns

* fix(tables): position-based gap rows, insert above/below, consistency fixes

- Fix gap row insert shifting: only shift rows when target position is
  occupied, preventing unnecessary displacement of rows below
- Switch to position-based indexing throughout (positionMap, maxPosition)
  instead of array-index for correct sparse position handling
- Add insert row above/below to context menu
- Use CellContent for pending values in PositionGapRows (matching PlaceholderRows)
- Add belowHeader selection overlay logic to PositionGapRows
- Remove unnecessary 500ms debounce on column width persistence

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

* fix cells nav w keyboard

* added preview panel for html, markdown rendering, completed table

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 16:37:48 -07:00
Emir Karabeg
8fd8b1a248 improvement(mothership): chat history and stability 2026-03-09 15:57:13 -07:00
Emir Karabeg
917af6d141 improvement(mothership): chat stability 2026-03-09 15:42:12 -07:00
Vikhyath Mondreti
fe5f809e1a Merge branch 'feat/mothership-copilot' of github.com:simstudioai/sim into feat/mothership-copilot 2026-03-09 15:28:07 -07:00
Vikhyath Mondreti
2f2c2b05e8 feat(templates): landing page templates workflow states 2026-03-09 15:22:14 -07:00
Siddharth Ganesan
0c44332172 File uploads to mothership 2026-03-09 15:19:56 -07:00
Siddharth Ganesan
7c0cd36936 Fix error status 2026-03-09 14:30:46 -07:00
Siddharth Ganesan
2788c68e45 Tool results 2026-03-09 14:24:44 -07:00
Siddharth Ganesan
cf9cc0377d Fix tool call persistence in chat 2026-03-09 14:24:44 -07:00
Emir Karabeg
a091149da4 improvement(mothership): worklfow resource 2026-03-09 14:20:59 -07:00
Waleed
64cedfcff7 fix(streaming): smoother streaming with throttled rendering, ResizeObserver scroll, and batched updates (#3471)
* fix(streaming): smoother streaming with throttled rendering, ResizeObserver scroll, and batched updates

- Add useThrottledValue hook (100ms trailing-edge throttle) to gate DOM re-renders during streaming across all chat surfaces
- Replace 100ms setInterval scroll polling with ResizeObserver-based auto-scroll, programmatic scroll timestamp tracking, and nested [data-scrollable] region handling
- Extract processContentBuffer from inline content handler for cleaner code organization in copilot SSE handlers
- Add RAF-based update batching (50ms max interval) to floating chat and home chat streaming paths
- Add useProgressiveList hook for progressive rendering of long conversation histories via requestAnimationFrame

Made-with: Cursor

* ack PR comments

* fix search modal

* more comments

* ack comments

* count

* ack comments

* ack comment
2026-03-09 13:27:33 -07:00
Vikhyath Mondreti
15db69231f fix tests 2026-03-09 12:22:40 -07:00
Vikhyath Mondreti
7b43091984 Merge branch 'feat/mothership-copilot' of github.com:simstudioai/sim into feat/mothership-copilot 2026-03-09 12:13:34 -07:00
Emir Karabeg
48f280427e feat(mothership): resource viewer 2026-03-09 12:07:06 -07:00
Vikhyath Mondreti
1430eb66de fix(landing): wire agent input to mothership 2026-03-09 11:58:16 -07:00
Siddharth Ganesan
2ace7252f9 Store tool call results 2026-03-09 11:35:20 -07:00
Siddharth Ganesan
bcdfc85ccb Tool updates 2026-03-09 11:28:22 -07:00
Vikhyath Mondreti
e921448bf2 fix(selections): more nested folder inaccuracies 2026-03-09 11:17:43 -07:00
Vikhyath Mondreti
71d8e227bd improvement(folder-selection): folder deselection + selection order should match visual 2026-03-09 11:00:22 -07:00
Siddharth Ganesan
4593a8a471 Table tools 2026-03-09 10:21:12 -07:00
Emir Karabeg
301fdb94ff improvement(tables): multi-select and efficiencies 2026-03-09 10:07:21 -07:00
Emir Karabeg
4afc3bbff8 improvement: logs 2026-03-09 09:13:01 -07:00
waleed
76981c356f update schedule creation ui and run lint 2026-03-09 02:18:43 -07:00
Waleed
4c562c8e04 feat(tables): column operations, row ordering, V1 API (#3488)
* feat(tables): add column operations, row ordering, V1 columns API, and OpenAPI spec

Adds column rename/delete/type change/constraint updates to the tables module,
row ordering via position column, UI metadata schema, V1 public API for column
operations with rate limiting and audit logging, and OpenAPI documentation.

Key changes:
- Service-layer column operations with validation (name pattern, type compatibility, unique/required constraints)
- Position column on user_table_rows with composite index for efficient ordering
- V1 /api/v1/tables/{tableId}/columns endpoint (POST/PATCH/DELETE) with rate limiting and audit
- Shared Zod schemas extracted to table/utils.ts using COLUMN_TYPES constant
- Targeted React Query invalidation (row vs schema mutations) with consistent onSettled usage
- OpenAPI 3.1.0 spec for columns endpoint with code samples
- Position field added to all row response mappings for consistency
- Sort fallback to position ordering when buildSortClause returns null

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

* fix(tables): use specific error prefixes instead of broad "Cannot" match

Prevents internal TypeErrors (e.g. "Cannot read properties of undefined")
from leaking as 400 responses. Now matches only domain-specific errors:
"Cannot delete the last column" and "Cannot set column".

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

* fix(tables): reject Infinity and NaN in number type compatibility check

Number.isFinite rejects Infinity, -Infinity, and NaN, preventing
non-finite values from passing column type validation.

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

* fix(tables): invalidate table list on row create/delete for stale rowCount

Row create and delete mutations now invalidate the table list cache since
it includes a computed rowCount. Row updates (which don't change count)
continue to only invalidate row queries.

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

* fix(tables): add column name length check, deduplicate name gen, reset pagination on clear

- Add MAX_COLUMN_NAME_LENGTH validation to addTableColumn (was missing,
  renameColumn already had it)
- Extract generateColumnName helper to eliminate triplicated logic across
  handleAddColumn, handleInsertColumnLeft, handleInsertColumnRight
- Reset pagination to page 0 when clearing sort/filter to prevent showing
  empty pages after narrowing filters are removed

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

* fix: hoist tableId above try block in V1 columns route, add detail invalidation to invalidateRowCount

- V1 columns route: `tableId` was declared inside `try` but referenced in
  `catch` logger.error, causing undefined in error logs. Hoisted `await params`
  above try in all three handlers (POST, PATCH, DELETE).
- invalidateRowCount: added `tableKeys.detail(tableId)` invalidation since the
  single-table GET response includes `rowCount`, which becomes stale after
  row create/delete without this.

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

* fix: add position to all row mutation responses, remove dead filter code

- Add `position` field to POST (single + batch) and PATCH row responses
  across both internal and V1 routes, matching GET responses and OpenAPI spec.
- Remove unused `filterConfig`, `handleFilterToggle`, `handleFilterClear`,
  and `activeFilters` — dead code left over from merge conflict resolution.
  `handleFilterApply` (the one actually wired to JSX) is preserved.

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

* fix: invalidateTableSchema now also invalidates table list cache

Column add/rename/delete/update mutations now invalidate tableKeys.list()
since the list endpoint returns schema.columns for each table. Without this,
the sidebar table list would show stale column schemas until staleTime expires.

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

* fix: replace window.prompt/confirm with emcn Modal dialogs

Replace non-standard browser dialogs with proper emcn Modal components
to match the existing codebase pattern (e.g. delete table confirmation).

- Column rename: Modal with Input field + Enter key support
- Column delete: Modal with destructive confirmation

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 02:14:38 -07:00
Emir Karabeg
d4eb25df91 fix(files): icon 2026-03-09 02:12:27 -07:00
Emir Karabeg
ac2af53884 improvement: icons 2026-03-09 02:10:54 -07:00
Emir Karabeg
016d353baf improvement: icon, resource header options 2026-03-09 01:47:08 -07:00
Emir Karabeg
d9c1a53cad improvement(resource): layout 2026-03-09 01:20:50 -07:00
Waleed
2bdc073d7b fix(docs): use named grid lines instead of numeric column indices (#3487)
Root cause: the fumadocs grid template has 3 columns in production but
5 columns in local dev. Our CSS used `grid-column: 3 / span 2` which
targeted the wrong column in the 3-column grid, placing content in
the near-zero-width TOC column instead of the main content column.

Fix: use `grid-column: main-start / toc-end` which uses CSS named grid
lines from grid-template-areas, working regardless of column count.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 00:50:28 -07:00
Waleed
13d2a134d0 improvement(docs): align sidebar method badges and polish API reference styling (#3484)
* improvement(docs): align sidebar method badges and polish API reference styling

* fix(docs): revert className prop on DocsPage for CI compatibility

* fix(docs): restore oneOf schema for delete rows and use rem units in CSS

* fix(docs): replace :has() selectors with direct className for reliable prod layout

The API docs layout was intermittently narrow in production because CSS
:has(.api-page-header) selectors are unreliable in Tailwind v4 production
builds. Apply className="openapi-page" directly to DocsPage and replace
all 64 :has() selectors with .openapi-page class targeting.

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

* fix(docs): bypass TypeScript check for className prop on DocsPage

Use spread with type assertion to pass className to DocsPage, working
around a CI type resolution issue where the prop exists at runtime but
is not recognized by TypeScript in the Vercel build environment.

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

* fix(docs): use inline style tag for grid layout, revert CSS to :has() selectors

The className prop on DocsPage doesn't exist in the fumadocs-ui version
resolved on Vercel, so .openapi-page was never applied and all 64 CSS
rules broke. Revert to :has(.api-page-header) selectors for styling and
use an inline <style> tag for the critical grid-column layout override,
which is SSR'd and doesn't depend on any CSS selector matching.

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

* fix(docs): add pill styling to footer navigation method badges

The footer nav badges (POST, GET, etc.) had color from data-method rules
but lacked the structural pill styling (padding, border-radius, font-size).

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 00:22:09 -07:00
Emir Karabeg
a61dc23d43 improvement: tables, dropdown 2026-03-09 00:17:52 -07:00
Waleed
12c1ede336 feat(files): inline file viewer with text editing (#3475)
* feat(files): add inline file viewer with text editing and create file modal

Add file preview/edit functionality to the workspace files page. Text files
(md, json, txt, yaml, etc.) open in an editable textarea with Cmd/Ctrl+S save.
PDFs render in an iframe. New file button creates empty .md files via a modal.
Uses ResourceHeader breadcrumbs and ResourceOptionsBar for save/download/delete.

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

* improvement(files): add UX polish, PR review fixes, and context menu

- Add unsaved changes guard modal (matching credentials manager pattern)
- Add delete confirmation modal for both viewer and context menu
- Add save status feedback (Save → Saving... → Saved)
- Add right-click context menu with Open, Download, Delete actions
- Add 50MB file size limit on content update API
- Add storage quota check before content updates
- Add response.ok guard on download to prevent corrupt files
- Add skeleton loading for pending file selection (prevents flicker)
- Fix updateContent in handleSave dependency array

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

* fix(files): propagate save errors and remove redundant sizeDiff

- Remove try/catch in TextEditor.handleSave so errors propagate to
  parent, which correctly shows save failure status
- Remove redundant inner sizeDiff declaration that shadowed outer scope

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

* fix(files): remove unused textareaRef

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

* fix(files): move Cmd+S to parent, add save error feedback, hide save for non-text files

- Move Cmd+S keyboard handler from TextEditor to Files so it goes
  through the parent handleSave with proper status management
- Add 'error' save status with red "Save failed" label that auto-resets
- Only show Save button for text-editable file types (md, txt, json, etc.)

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

* improvement(files): add save tooltip, deduplicate text-editable extensions

- Add Tooltip on Save button showing Cmd+S / Ctrl+S shortcut
- Export TEXT_EDITABLE_EXTENSIONS from file-viewer and reuse in files.tsx
  instead of duplicating the list inline

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

* refactor: extract isMacPlatform to shared utility

Move isMacPlatform() from global-commands-provider.tsx to
lib/core/utils/platform.ts so it can be reused by files.tsx tooltip
without duplication.

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

* refactor(files): deduplicate delete modal, use shared formatFileSize

- Extract DeleteConfirmModal component to eliminate duplicate modal
  markup between viewer and list modes
- Replace local formatFileSize with shared utility from file-utils.ts

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

* fix(files): fix a11y label lint error and remove mutation object from useCallback deps

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

* fix(files): add isDirty guard on handleSave, return proper HTTP status codes

Prevents "Saving → Saved" flash when pressing Cmd+S with no changes.
Returns 404 for file-not-found and 402 for quota-exceeded instead of 500.

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

* fix(files): reset isDirty/saveStatus on delete and discard, remove deprecated navigator.platform

- Clear isDirty and saveStatus when deleting the currently-viewed file to
  prevent spurious beforeunload prompts
- Reset saveStatus on discard to prevent stale "Save failed" when opening
  another file
- Remove deprecated navigator.platform, userAgent fallback covers all cases

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

* fix(files): prevent concurrent saves on rapid Cmd+S, add YAML MIME types

- Add saveStatus === 'saving' guard to handleSave to prevent duplicate
  concurrent PUT requests from rapid keyboard shortcuts
- Add yaml/yml MIME type mappings to getMimeTypeFromExtension

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

* refactor(files): reuse shared extension constants, parallelize cancelQueries

- Replace hand-rolled SUPPORTED_EXTENSIONS with composition from existing
  SUPPORTED_DOCUMENT/AUDIO/VIDEO_EXTENSIONS in validation.ts
- Parallelize sequential cancelQueries calls in delete mutation onMutate

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

* fix(files): guard handleCreate against duplicate calls while pending

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

* fix(files): show upload progress on the Upload button, not New file

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

* fix(files): use ref-based guard for create pending state to avoid stale closure

The uploadFile.isPending check was stale because the mutation object
is excluded from useCallback deps (per codebase convention). Using a
ref ensures the guard works correctly across rapid Enter key presses.

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

* cleanup(files): use shared icon import, remove no-op props, wrap handler in useCallback

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 00:07:35 -07:00
Emir Karabeg
627eaaf343 improvement: tables, favicon 2026-03-08 19:21:21 -07:00
Waleed Latif
1dbfaa4d23 style(schedules): apply linter formatting 2026-03-08 18:41:29 -07:00
Waleed Latif
4946571922 feat(schedules): add edit support with context menu for standalone jobs 2026-03-08 18:41:17 -07:00
Waleed Latif
6295fd1a11 feat(schedules): add schedule creator modal for standalone jobs
Add modal to create standalone scheduled jobs from the Schedules page.
Includes POST API endpoint, useCreateSchedule mutation hook, and full
modal with schedule type selection, timezone, lifecycle, and live preview.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 18:17:11 -07:00
Emir Karabeg
de5faa5265 improvement(tables): consolidation 2026-03-08 17:31:58 -07:00
Waleed
7d360649e9 fix(sidebar): restore drag-and-drop for workflows and folders (#3470)
* fix(sidebar): restore drag-and-drop for workflows and folders

Made-with: Cursor

* update docs, unrelated
2026-03-08 16:16:14 -07:00
Vikhyath Mondreti
1d955fc43a feat(mothership): billing (#3464)
* Billing update

* more billing improvements

* credits UI

* credit purchase safety

* progress

* ui improvements

* fix cancel sub

* fix types

* fix daily refresh for teams

* make max features differentiated

* address bugbot comments

* address greptile comments

* revert isHosted

* address more comments

* fix org refresh bar

* fix ui rounding

* fix minor rounding

* fix upgrade issue for legacy plans

* fix formatPlanName

* fix email dispay names

* fix legacy team reference bugs

* referral bonus in credits

* fix org upgrade bug

* improve logs

* respect toggle for paid users

* fix landing page pro features and usage limit checks

* fixed query and usage

* add unit test

* address more comments

* enterprise guard

* fix limits bug

* pass period start/end for overage
2026-03-08 03:37:54 -07:00
Waleed
1def94392b improvement(settings): fix mcp modal, add option to edit JSON and add Sim as an MCP client (#3467)
* improvement(settings): fix mcp modal, add option to edit JSON and add Sim as an MCP client

* added docs link in sidebar

* ack comments

* ack comments

* fixed error msg
2026-03-07 22:59:29 -08:00
Emir Karabeg
77bd2553f2 fix(resource): sorting 2026-03-07 21:26:54 -08:00
Emir Karabeg
8170488488 improvement(resource): sorting and icons 2026-03-07 21:19:04 -08:00
Waleed
0b42e26f10 fix(execution): ensure background tasks await post-execution DB status updates (#3466)
The fire-and-forget IIFE in execution-core.ts for post-execution logging could be abandoned when trigger.dev tasks exit, leaving executions permanently stuck in "running" status. Store the promise on LoggingSession so background tasks can optionally await it before returning.
2026-03-07 21:09:31 -08:00
Emir Karabeg
4b7a9b20c4 improvement(resources): segmented API 2026-03-07 20:48:08 -08:00
Waleed
76486ebcc8 feat(knowledge): add v1 knowledge base API, Obsidian/Evernote connectors, and docs (#3465)
* feat(knowledge): add v1 knowledge base API, Obsidian/Evernote connectors, and docs

- Add v1 REST API for knowledge bases (CRUD, document management, vector search)
- Add Obsidian and Evernote knowledge base connectors
- Add file type validation to v1 file and document upload endpoints
- Update OpenAPI spec with knowledge base endpoints and schemas
- Add connectors documentation page
- Apply query hook formatting improvements

* fix(knowledge): address PR review feedback

- Remove validateFileType from v1/files route (general file upload, not document-only)
- Reject tag filters when searching multiple KBs (tag defs are KB-specific)
- Cache tag definitions to avoid duplicate getDocumentTagDefinitions call
- Fix Obsidian connector silent empty results when syncContext is undefined

* improvement(connectors): add syncContext to getDocument, clean up caching

- Update docs to say 20+ connectors
- Add syncContext param to ConnectorConfig.getDocument interface
- Use syncContext in Evernote getDocument to cache tag/notebook maps
- Replace index-based cache check with Map keyed by KB ID in search route

* fix(knowledge): address second round of PR review feedback

- Fix Zod .default('text') overriding tag definition's actual fieldType
- Fix encodeURIComponent breaking multi-level folder paths in Obsidian
- Use 413 instead of 400 for file-too-large in document upload
- Add knowledge-bases to API reference docs navigation

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

* fix(knowledge): prevent cross-workspace KB access in search

Filter accessible KBs by matching workspaceId from the request,
preventing users from querying KBs in other workspaces they have
access to but didn't specify.

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

* fix(knowledge): audit resourceId, SSRF protection, recursion depth limit

- Fix recordAudit using knowledgeBaseId instead of newDocument.id
- Add SSRF validation to Obsidian connector (reject private/loopback URLs)
- Add max recursion depth (20) to listVaultFiles

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

* fix(obsidian): remove SSRF check that blocks localhost usage

The Obsidian connector is designed to connect to the Local REST API
plugin running on localhost (127.0.0.1:27124). The SSRF check was
incorrectly blocking this primary use case.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 20:00:38 -08:00
Emir Karabeg
13d49da8bd improvement(resources): layout and items 2026-03-07 18:01:29 -08:00
Waleed
05b8481a89 fix(knowledge): compute KB tokenCount from documents instead of stale column (#3463)
The knowledge_base.token_count column was initialized to 0 and never
updated. Replace with COALESCE(SUM(document.token_count), 0) in all
read queries, which already JOIN on documents with GROUP BY.
2026-03-07 16:55:06 -08:00
Emir Karabeg
6690c55721 improvement(resource): layout 2026-03-07 16:25:53 -08:00
Siddharth Ganesan
88a8c5f4a1 Update mothership to match copilot in logs 2026-03-07 16:18:34 -08:00
Siddharth Ganesan
91ca6a531e Fix tables row count 2026-03-07 16:04:36 -08:00
Emir Karabeg
2f45f935e4 ran lint 2026-03-07 15:47:53 -08:00
Waleed
2cb12de546 refactor(queries): comprehensive TanStack Query best practices audit (#3460)
* refactor: comprehensive TanStack Query best practices audit and migration

- Add AbortSignal forwarding to all 41 queryFn implementations for proper request cancellation
- Migrate manual fetch patterns to useMutation hooks (useResetPassword, useRedeemReferralCode, usePurchaseCredits, useImportWorkflow, useOpenBillingPortal, useAllowedMcpDomains)
- Migrate standalone hooks to TanStack Query (use-next-available-slot, use-mcp-server-test, use-webhook-management, use-referral-attribution)
- Fix query key factories: add missing `all` keys, replace inline keys with factory methods
- Fix optimistic mutations: use onSettled instead of onSuccess for cache reconciliation
- Replace overly broad cache invalidations with targeted key invalidation
- Remove keepPreviousData from static-key queries where it provides no benefit
- Add staleTime to queries missing explicit cache duration
- Fix `any` type in UpdateSettingParams with proper GeneralSettings typing
- Remove dead code: loadingWebhooks/checkedWebhooks from subblock store, unused helper functions
- Update settings components (general, debug, referral-code, credit-balance, subscription, mcp) to use mutation state instead of manual useState for loading/error/success

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

* fix: remove unstable mutation object from useCallback deps

openBillingPortal mutation object is not referentially stable,
but .mutate() is stable in TanStack Query v5. Remove from deps
to prevent unnecessary handleBadgeClick recreations.

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

* fix: add missing byWorkflows invalidation to useUpdateTemplate

The onSettled handler was missing the byWorkflows() invalidation
that was dropped during the onSuccess→onSettled migration. Without
this, the deploy modal (useTemplateByWorkflow) would show stale data
after a template update.

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

* docs: add TanStack Query best practices to CLAUDE.md and cursor rules

Add comprehensive React Query best practices covering:
- Hierarchical query key factories with intermediate plural keys
- AbortSignal forwarding in all queryFn implementations
- Targeted cache invalidation over broad .all invalidation
- onSettled for optimistic mutation cache reconciliation
- keepPreviousData only on variable-key queries
- No manual fetch in components rule
- Stable mutation references in useCallback deps

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

* fix: address PR review feedback

- Fix syncedRef regression in use-webhook-management: only set
  syncedRef.current=true when webhook is found, so re-sync works
  after webhook creation (e.g., post-deploy)
- Remove redundant detail(id) invalidation from useUpdateTemplate
  onSettled since onSuccess already populates cache via setQueryData

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

* fix: address second round of PR review feedback

- Reset syncedRef when blockId changes in use-webhook-management so
  component reuse with a different block syncs the new webhook
- Add response.ok check in postAttribution so non-2xx responses
  throw and trigger TanStack Query retry logic

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

* fix: use lists() prefix invalidation in useCreateWorkspaceCredential

Use workspaceCredentialKeys.lists() instead of .list(workspaceId) so
filtered list queries are also invalidated on credential creation,
matching the pattern used by update and delete mutations.

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

* fix: address third round of PR review feedback

- Add nullish coalescing fallback for bonusAmount in referral-code
  to prevent rendering "undefined" when server omits the field
- Reset syncedRef when queryEnabled becomes false so webhook data
  re-syncs when the query is re-enabled without component remount

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

* fix: address fourth round of PR review feedback

- Add AbortSignal to testMcpServerConnection for consistency
- Wrap handleTestConnection in try/catch for mutateAsync error handling
- Replace broad subscriptionKeys.all with targeted users()/usage() invalidation
- Add intermediate users() key to subscription key factory for prefix matching
- Add comment documenting syncedRef null-webhook behavior
- Fix api-keys.ts silent error swallowing on non-ok responses
- Move deployments.ts cache invalidation from onSuccess to onSettled

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

* fix: achieve full TanStack Query best practices compliance

- Add intermediate plural keys to api-keys, deployments, and schedules
  key factories for prefix-based invalidation support
- Change copilot-keys from refetchQueries to invalidateQueries
- Add signal parameter to organization.ts fetch functions (better-auth
  client does not support AbortSignal, documented accordingly)
- Move useCreateMcpServer invalidation from onSuccess to onSettled

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 15:15:10 -08:00
Emir Karabeg
de32644940 improvement(resources): all outer page structure complete 2026-03-07 14:42:11 -08:00
Emir Karabeg
8ff93fe842 improvement(resource): tables, files 2026-03-07 13:42:22 -08:00
Waleed
0d9e04181f improvement(perf): apply react and js performance optimizations across codebase (#3459)
* improvement(perf): apply react and js performance optimizations across codebase

- Parallelize independent DB queries with Promise.all in API routes
- Defer PostHog and OneDollarStats via dynamic import() to reduce bundle size
- Use functional setState in countdown timers to prevent stale closures
- Replace O(n*m) .filter().find() with Set-based O(n) lookups in undo-redo
- Use .toSorted() instead of .sort() for immutable state operations
- Use lazy initializers for useState(new Set()) across 20 components
- Remove useMemo wrapping trivially cheap expressions (typeof, ternary, template strings)
- Add passive: true to scroll event listener

* fix(perf): address PR review feedback

- Extract IIFE Set patterns to named consts for readability in use-undo-redo
- Hoist Set construction above loops in BATCH_UPDATE_PARENT cases
- Add .catch() error handler to PostHog dynamic import
- Convert session-provider posthog import to dynamic import() to complete bundle split

* fix(analytics): add .catch() to onedollarstats dynamic import
2026-03-07 13:08:26 -08:00
Waleed
1324987def improvement(turbo): align turborepo config with best practices (#3458)
* improvement(turbo): align turborepo config with best practices

* fix(turbo): address PR review feedback

* fix(turbo): add lint:check task for read-only lint+format CI checks

lint:check previously delegated to format:check which only checked
formatting. Now it runs biome check (no --write) which enforces both
lint rules and formatting without mutating files.

* upgrade turbo
2026-03-07 12:38:46 -08:00
Waleed
9c4abf7b9b fix(connectors): add rate limiting, concurrency controls, and bug fixes (#3457)
* fix(connectors): add rate limiting, concurrency controls, and bug fixes across knowledge connectors

- Add Retry-After header support to fetchWithRetry for all 18 connectors
- Batch concurrent API calls (concurrency 5) in Dropbox, Google Docs, Google Drive, OneDrive, SharePoint
- Batch concurrent API calls (concurrency 3) in Notion to match 3 req/s limit
- Cache GitHub tree in syncContext to avoid re-fetching on every pagination page
- Batch GitHub blob fetches with concurrency 5
- Fix GitHub base64 decoding: atob() → Buffer.from() for UTF-8 safety
- Fix HubSpot OAuth scope: 'tickets' → 'crm.objects.tickets.read' (v3 API)
- Fix HubSpot syncContext key: totalFetched → totalDocsFetched for consistency
- Add jitter to nextSyncAt (10% of interval, capped at 5min) to prevent thundering herd
- Fix Date consistency in connector DELETE route

* fix(connectors): address PR review feedback on retry and SharePoint batching

- Remove 120s cap on Retry-After — pass all values through to retry loop
- Add maxDelayMs guard: if Retry-After exceeds maxDelayMs, throw immediately
  instead of hammering with shorter intervals (addresses validate timeout concern)
- Add early exit in SharePoint batch loop when maxFiles limit is reached
  to avoid unnecessary API calls

* fix(connectors): cap Retry-After at maxDelayMs instead of aborting

Match Google Cloud SDK behavior: when Retry-After exceeds maxDelayMs,
cap the wait to maxDelayMs and log a warning, rather than throwing
immediately. This ensures retries are bounded in duration while still
respecting server guidance within the configured limit.

* fix(connectors): add early-exit guard to Dropbox, Google Docs, OneDrive batch loops

Match the SharePoint fix — skip remaining batches once maxFiles limit
is reached to avoid unnecessary API calls.
2026-03-07 12:12:15 -08:00
Siddharth Ganesan
00c9b72bdd Fix lint 2026-03-07 12:06:12 -08:00
Siddharth Ganesan
386df7a062 Fix 2026-03-07 11:47:53 -08:00
Siddharth Ganesan
0967755ad4 Clean vfs 2026-03-07 11:29:20 -08:00
Siddharth Ganesan
b50ccdf314 Fixes 2026-03-07 11:14:13 -08:00
Siddharth Ganesan
7247a5f4d8 Fixes 2026-03-07 10:43:41 -08:00
Waleed Latif
875498c9aa fix: resolve post-merge test and lint failures
- airtable: sync tableSelector condition with tableId (add getSchema)
- backfillCanonicalModes test: add documentId mode to prevent false backfill
- schedule PUT test: use invalid action string now that disable is valid
- schedule execute tests: add ne mock, sourceType field, use
  mockReturnValueOnce for two db.update calls
- knowledge tools: fix biome formatting (single-line arrow functions)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 09:49:57 -08:00
Waleed Latif
3c196d180f lint 2026-03-07 01:45:56 -08:00
Waleed Latif
2940de946c fix: correct knowledge block canonical pair pattern and subblock migration
- Rename manualDocumentId to documentId (advanced subblock ID should match
  canonicalParamId, consistent with airtable/gmail patterns)
- Fix documentSelector.dependsOn to reference knowledgeBaseSelector (basic
  depends on basic, not advanced)
- Remove unnecessary documentId migration (ID unchanged from main)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 01:38:05 -08:00
Waleed Latif
212f912827 Merge staging into feat/mothership-copilot
Resolved conflicts:
- oauth-required-modal.tsx: removed local SCOPE_DESCRIPTIONS (moved to lib/oauth/utils)
- credential-selector.tsx (2 files): kept useSettingsNavigation import, removed duplicate getMissingRequiredScopes
- airtable.ts: combined HEAD's dependsOn/getSchema with staging's mode:'advanced'

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 01:25:48 -08:00
Waleed
1ba1bc8edb feat(evernote): add Evernote integration with 11 tools (#3456)
* feat(evernote): add Evernote integration with 11 tools

* fix(evernote): fix signed integer mismatch in Thrift version check

* fix(evernote): fix exception field mapping and add sandbox support

* fix(evernote): address PR review feedback

* fix(evernote): clamp maxNotes to Evernote's 250 limit

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 00:52:57 -08:00
Emir Karabeg
9a505919b0 refactor, improvement 2026-03-06 23:44:34 -08:00
Waleed
53fd92a30a feat(obsidian): add Obsidian integration with 15 tools (#3455)
* feat(obsidian): add Obsidian integration with 15 tools

* fix(obsidian): encode path segments individually to preserve slashes

* improvement(obsidian): add type re-exports and improve output descriptions

* fix(obsidian): remove unreachable 404 handling from transformResponse
2026-03-06 23:13:47 -08:00
Waleed
0a52b09deb feat(jira): add search_users tool for user lookup by email (#3451)
* feat(jira): add search_users tool for user lookup by email

* improvement(jira): reuse shared transformUser utility in search_users

* improvement(jira): add pagination fields to search_users response

* update

* fix(jira): filter falsy entries before transforming search_users results

* fix(jira): add defensive fallback for nullable transformUser in search_users

* fix(jira): align search_users response type with transformUser return type
2026-03-06 19:52:37 -08:00
Vikhyath Mondreti
1d36b80172 improvement(selectors): remove dead semantic fallback code (#3454)
* improvement(selectors): simplify selectorContext + add tests

* fix resolve values fallback

* another workflowid pass through

* remove dead code

* make workspace id required
2026-03-06 19:38:57 -08:00
Vikhyath Mondreti
e6a5e7f4e4 improvement(selectors): simplify selector context + add tests (#3453)
* improvement(selectors): simplify selectorContext + add tests

* fix resolve values fallback

* another workflowid pass through
2026-03-06 18:30:46 -08:00
Waleed
e6ca3b3311 feat(knowledge): add connector tools and expand document metadata (#3452)
* feat(knowledge): add connector tools and expand document metadata

* fix(knowledge): address PR review feedback on new tools

* fix(knowledge): remove unused params from get_document transform
2026-03-06 17:58:33 -08:00
Waleed
b93c87c521 fix(fireflies): correct types from live API validation (#3450)
* fix(fireflies): correct types from live API validation

- speakers.id is number, not string (API returns 0, 1, 2...)
- summary.action_items is a single string, not string[]
- Update formatTranscriptContent to handle action_items as string

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

* fix(fireflies): correct tool types from live API validation

- FirefliesSpeaker.id: string -> number
- FirefliesSentence.speaker_id: string -> number
- FirefliesSpeakerAnalytics.speaker_id: string -> number
- FirefliesSummary.action_items: string[] -> string
- FirefliesSummary.outline: string[] -> string
- FirefliesSummary.shorthand_bullet: string[] -> string
- FirefliesSummary.bullet_gist: string[] -> string
- FirefliesSummary.topics_discussed: string[] -> string

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 17:46:42 -08:00
Siddharth Ganesan
06a4a8162a Add context 2026-03-06 17:11:18 -08:00
Waleed
a71304200e improvement(oauth): centralize scopes and remove dead scope evaluation code (#3449)
* improvement(oauth): centralize scopes and remove dead scope evaluation code

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

* fix(oauth): fix stale scope-descriptions.ts references and add test coverage

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 17:08:25 -08:00
Waleed
96c2ae2c39 feat(connectors): add Fireflies connector and API key auth support (#3448)
* feat(connectors): add Fireflies connector and API key auth support

Extend the connector system to support both OAuth and API key authentication
via a discriminated union (`ConnectorAuthConfig`). Add Fireflies as the first
API key connector, syncing meeting transcripts via the Fireflies GraphQL API.

Schema changes:
- Make `credentialId` nullable (null for API key connectors)
- Add `encryptedApiKey` column (AES-256-GCM encrypted, null for OAuth)

This eliminates the `'_apikey_'` sentinel and inline `sourceConfig._encryptedApiKey`
patterns, giving each auth mode its own clean column.

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

* fix(fireflies): allow 0 for maxTranscripts (means unlimited)

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 16:48:39 -08:00
Vikhyath Mondreti
a4d581c76f improvement(canonical): backfill for canonical modes on config changes (#3447)
* improvement(canonical): backfill for canonical modes on config changes

* persist data changes to db
2026-03-06 16:17:14 -08:00
Waleed
f1efc598d1 fix(selectors): resolve env var references at design time for selector context (#3446)
* fix(selectors): resolve env var references at design time for selector context

Selectors now resolve {{ENV_VAR}} references before building context and
returning dependency values to consumers, enabling env-var-based credentials
(e.g. {{SLACK_BOT_TOKEN}}) to work with selector dropdowns.

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

* fix(selectors): prevent unresolved env var templates from leaking into context

- Fall back to undefined instead of raw template string when env var is
  missing from store, so the null-check in the context loop discards it
- Use resolvedDetailId in query cache key so React Query refetches when
  the underlying env var value changes

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

* fix(selectors): use || for consistent empty-string env var handling

Align use-selector-setup.ts with use-selector-query.ts by using || instead
of ?? so empty-string env var values are treated as unset.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 15:53:00 -08:00
Theodore Li
1e53d5748a Fix oauth link callback from mothership task 2026-03-06 13:46:08 -08:00
Waleed Latif
6d803bcde2 fix(knowledge): pass workspaceId to useOAuthCredentials in connector card
The ConnectorCard was calling useOAuthCredentials(providerId) without
a workspaceId, causing the credentials API to return an empty array.
This meant the credential lookup always failed, getMissingRequiredScopes
received undefined, and the "Update access" banner always appeared.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:08:14 -08:00
Waleed Latif
bca131d597 fix(connectors): restore Linear connector requiredScopes
Linear OAuth does return scopes in the token response. The previous
fix of emptying requiredScopes was based on an incorrect assumption.
Restoring requiredScopes: ['read'] as it should work correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:55:06 -08:00
Waleed Latif
0202c60d26 Revert "fix(connectors): remove legacy requiredScopes from Jira and Confluence connectors"
This reverts commit a0be3ff414.
2026-03-06 12:52:29 -08:00
Waleed
82ba3d7dd1 feat(tasks): add rename to task context menu (#3442) 2026-03-06 12:49:32 -08:00
Waleed Latif
a0be3ff414 fix(connectors): remove legacy requiredScopes from Jira and Confluence connectors
Jira and Confluence OAuth tokens don't return legacy scope names like
read:jira-work or read:confluence-content.all, causing the 'Update access'
banner to always appear. Set requiredScopes to empty array like Linear.
2026-03-06 12:44:46 -08:00
Waleed Latif
6cda9e60e8 fix(connectors): remove unverifiable requiredScopes for Linear connector 2026-03-06 12:40:15 -08:00
Waleed
244cf4ff7e feat(selectors): add dropdown selectors for 14 integrations (#3433)
* feat(selectors): add dropdown selectors for 14 integrations

* fix(selectors): secure OAuth tokens in JSM and Confluence selector routes

Convert JSM selector-servicedesks, selector-requesttypes, and Confluence
selector-spaces routes from GET (with access token in URL query params) to
POST with authorizeCredentialUse + refreshAccessTokenIfNeeded pattern. Also
adds missing ensureCredential guard to microsoft.planner.plans registry entry.

* fix(selectors): use sanitized serviceDeskId and encode SharePoint siteId

Use serviceDeskIdValidation.sanitized instead of raw serviceDeskId in JSM
request types URL. Add encodeURIComponent to SharePoint siteId to prevent
URL path injection.

* lint

* fix(selectors): revert encodeURIComponent on SharePoint siteId

SharePoint site IDs use the format "hostname,guid,guid" with commas that
must remain unencoded for the Microsoft Graph API. The encodeURIComponent
call would convert commas to %2C and break the API call.

* fix(selectors): use sanitized cloudId in Confluence and JSM route URLs

Use cloudIdValidation.sanitized instead of raw cloudId in URL construction
for consistency with the validation pattern, even though the current
validator returns the input unchanged.

* fix(selectors): add missing context fields to resolution, ensureCredential to sharepoint.lists, and siteId validation

- Add baseId, datasetId, serviceDeskId to SelectorResolutionArgs,
  ExtendedSelectorContext, extractExtendedContext, useSelectorDisplayName,
  and resolveSelectorForSubBlock so cascading selectors resolve correctly
  through the resolution path.
- Add ensureCredential guard to sharepoint.lists registry entry.
- Add regex validation for SharePoint siteId format (hostname,GUID,GUID).

* fix(selectors): rename advanced subBlock IDs to avoid canonicalParamId clashes

Rename all advanced-mode subBlock IDs that matched their canonicalParamId
to use a `manual*` prefix, following the established convention
(e.g., manualSiteId, manualCredential). This prevents ambiguity between
subBlock IDs and canonical parameter names in the serialization layer.

25 renames across 14 blocks: baseId→manualBaseId, tableId→manualTableId,
workspace→manualWorkspace, objectType→manualObjectType, etc.

* Revert "fix(selectors): rename advanced subBlock IDs to avoid canonicalParamId clashes"

This reverts commit 4e30161c68.

* fix(selectors): rename canonicalParamIds to avoid subBlock ID clashes

Prefix all clashing canonicalParamId values with `selected_` so they
don't match any subBlock ID. Update each block's `inputs` section and
`tools.config.params` function to destructure the new canonical names
and remap them to the original tool param names. SubBlock IDs and tool
definitions remain unchanged for backwards compatibility.

Affected: 25 canonical params across 14 blocks (airtable, asana, attio,
calcom, confluence, google_bigquery, google_tasks, jsm, microsoft_planner,
notion, pipedrive, sharepoint, trello, zoom).

* fix(selectors): rename pre-existing driveId and files canonicalParamIds in SharePoint

Apply the same selected_ prefix convention to the pre-existing SharePoint
driveId and files canonical params that clashed with their subBlock IDs.

* style: format long lines in calcom, pipedrive, and sharepoint blocks

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

* fix(selectors): resolve cascading context for selected_ canonical params and normalize Asana response

Strip `selected_` prefix from canonical param IDs when mapping to
SelectorContext fields so cascading selectors (Airtable base→table,
BigQuery dataset→table, JSM serviceDesk→requestType) correctly
propagate parent values.

Normalize Asana workspaces route to return `{ id, name }` instead of
`{ gid, name }` for consistency with all other selector routes.

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

* fix(selectors): replace hacky prefix stripping with explicit CANONICAL_TO_CONTEXT mapping

Replace CONTEXT_FIELD_SET (Record<string, true>) with CANONICAL_TO_CONTEXT
(Record<string, keyof SelectorContext>) that explicitly maps canonical
param IDs to their SelectorContext field names.

This properly handles the selected_ prefix aliases (e.g. selected_baseId
→ baseId) without string manipulation, and removes the unsafe
Record<string, unknown> cast.

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

* refactor(selectors): remove unnecessary selected_ prefix from canonicalParamIds

The selected_ prefix was added to avoid a perceived clash between
canonicalParamId and subBlock id values, but this clash does not
actually cause any issues — pre-existing blocks on main (Google Sheets,
Webflow, SharePoint) already use matching values successfully.

Remove the prefix from all 14 blocks, revert use-selector-setup.ts to
the simple CONTEXT_FIELD_SET pattern, and simplify tools.config.params
functions that were only remapping the prefix back.

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

* fix(selectors): add spaceId selector pair to Confluence V2 block

The V2 block was missing the spaceSelector basic-mode selector that the
V1 (Legacy) block already had.

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

* refactor(selectors): revert V1 block changes, add selectors to Notion V1 for V2 inheritance

Confluence V1: reverted to main state (V2 has its own subBlocks).
Notion V1: added selector pairs per-operation since V2 inherits
subBlocks, inputs, and params from V1.

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

* fix(selectors): audit fixes for auth patterns, registry gaps, and display name resolution

- Convert Microsoft Planner plans/tasks routes from GET+getSession to POST+authorizeCredentialUse
- Add fetchById to microsoft.planner (tasks) and sharepoint.sites registry entries
- Add ensureCredential to sharepoint.sites and microsoft.planner registry fetchList
- Update microsoft.planner.plans registry to use POST method
- Add siteId, collectionId, spreadsheetId, fileId to SelectorDisplayNameArgs and caller
- Add fileId to SelectorResolutionArgs and resolution context
- Fix Zoom topicUpdate visibility in basic mode (remove mode:'advanced')
- Change Zoom meetings selector to fetch upcoming_meetings instead of only scheduled

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

* style: lint formatting fixes

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

* fix(selectors): consolidate Notion canonical param pairs into array conditions

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

* fix(selectors): add missing selectorKey to Confluence V1 page selector

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

* fix(selectors): use sanitized IDs in URLs, convert SharePoint routes to POST+authorizeCredentialUse

- Use planIdValidation.sanitized in MS Planner tasks fetch URL
- Convert sharepoint/lists and sharepoint/sites from GET+getSession to POST+authorizeCredentialUse
- Update registry entries to match POST pattern

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

* fix(selectors): revert Zoom meetings type to scheduled for broader compatibility

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

* fix(selectors): add SharePoint site ID validator, fix cascading selector display name fallbacks

- Add validateSharePointSiteId to input-validation.ts
- Use validation util in SharePoint lists route instead of inline regex
- Add || fallback to selector IDs in workflow-block.tsx so cascading
  display names resolve in basic mode (baseSelector, planSelector, etc.)

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

* fix(selectors): hoist requestId before try block in all selector routes

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

* fix(selectors): hoist requestId before try block in Trello boards route

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

* fix(selectors): guard selector queries against unresolved variable references

Skip fetchById and context population when values are design-time
placeholders (<Block.output> or {{ENV_VAR}}) rather than real IDs.

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

* refactor(selectors): replace hardcoded display name fallbacks with canonical-aware resolution

Use resolveDependencyValue to resolve context values for
useSelectorDisplayName, eliminating manual || getStringValue('*Selector')
fallbacks that required updating for each new selector pair.

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

* fix(selectors): tighten SharePoint site ID validation to exclude underscores

SharePoint composite site IDs use hostname,guid,guid format where only
alphanumerics, periods, hyphens, and commas are valid characters.

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

* fix(selectors): ensure string IDs in Pipedrive/Cal.com routes, fix Trello closed board filter

Pipedrive pipelines and Cal.com event-types/schedules routes now
consistently return string IDs via String() conversion.

Trello boards route no longer filters out closed boards, preserving
them for fetchById lookups. The closed filter is applied only in the
registry's fetchList so archived boards don't appear in dropdowns
but can still be resolved by ID for display names.

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

* fix(selectors): convert Zoom meeting IDs to strings for consistency

Zoom API returns numeric meeting IDs. Convert with String() to match
the string ID convention used by all other selector routes.

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

* fix(selectors): align registry types with route string ID returns

Routes already convert numeric IDs to strings via String(), so update
the registry types (CalcomEventType, CalcomSchedule, PipedrivePipeline,
ZoomMeeting) from id: number to id: string and remove the now-redundant
String() coercions in fetchList/fetchById.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:34:28 -08:00
Siddharth Ganesan
a6abb9da67 Job logs 2026-03-06 12:30:33 -08:00
Waleed
ae887185a1 fix(memory): upgrade bun from 1.3.9 to 1.3.10 (#3441) 2026-03-06 11:35:46 -08:00
Siddharth Ganesan
777e4f57de Job exeuction logs 2026-03-06 11:34:48 -08:00
Waleed
695628de75 improvement(knowledge): make connector-synced document chunks readonly (#3440)
* improvement(knowledge): make connector-synced document chunks readonly

* fix(knowledge): enforce connector chunk readonly on server side

* fix(knowledge): disable toggle and delete actions for connector-synced chunks
2026-03-06 11:29:28 -08:00
Siddharth Ganesan
d71e4e51ea Fix mothership block logs 2026-03-06 10:57:16 -08:00
Siddharth Ganesan
576d9d3025 Mothership block logs 2026-03-06 10:46:30 -08:00
Waleed
43509374a2 fix(sidebar): use client-generated UUIDs for stable optimistic updates (#3439)
* fix(sidebar): use client-generated UUIDs for stable optimistic updates

* fix(folders): use zod schema validation for folder create API

Replace inline UUID regex with zod schema validation for consistency
with other API routes. Update test expectations accordingly.

* fix(sidebar): add client UUID to single workflow duplicate hook

The useDuplicateWorkflow hook was missing newId: crypto.randomUUID(),
causing the same temp-ID-swap issue for single workflow duplication
from the context menu.

* fix(folders): avoid unnecessary Set re-creation in replaceOptimisticEntry

Only create new expandedFolders/selectedFolders Sets when tempId
differs from data.id. In the common happy path (client-generated UUIDs),
this avoids unnecessary Zustand state reference changes and re-renders.
2026-03-06 06:35:19 -08:00
Waleed
06c88441f8 fix(tool-input): restore workflow input mapper visibility (#3438) 2026-03-06 05:51:27 -08:00
Emir Karabeg
0e7c719e82 improvement(sidebar): loading 2026-03-06 02:35:24 -08:00
Siddharth Ganesan
226a3f64fb Fix lint 2026-03-05 22:10:27 -08:00
Waleed
127968d467 feat(slack): add views.open, views.update, views.push, views.publish tools (#3436)
* feat(slack): add views.open, views.update, views.push, views.publish tools

* feat(slack): wire view tools into slack block definition
2026-03-05 22:10:02 -08:00
Siddharth Ganesan
6c6b3579c9 Triggers in the vfs 2026-03-05 20:57:58 -08:00
Siddharth Ganesan
a5b148e19e Native kb connectors 2026-03-05 20:17:23 -08:00
Emir Karabeg
9665f49492 fix(workflow): editor visible 2026-03-05 20:07:44 -08:00
Waleed
2722f0efbf feat(reddit): add 5 new tools, fix bugs, and audit all endpoints against API docs (#3434)
* feat(reddit): add 5 new tools, fix bugs, and audit all endpoints against API docs

* fix(reddit): add optional chaining, pagination wiring, and trim safety

- Add optional chaining on children?.[0] in get_posts, get_controversial,
  search, and get_comments to prevent TypeError on unexpected API responses
- Wire after/before pagination params to get_messages block operation
- Use ?? instead of || for get_comments limit to handle 0 correctly
- Add .trim() on postId in get_comments URL path

* chore(reddit): remove unused output property constants from types.ts

* fix(reddit): add HTTP error handling to GET tools

Add !response.ok guards to get_me, get_user, get_subreddit_info,
and get_messages to return success: false on non-2xx responses
instead of silently returning empty data with success: true.

* fix(reddit): add input validation and HTTP error guards

- Add validateEnum/validatePathSegment to prevent URL path traversal
- Add !response.ok guards to send_message and reply tools
- Centralize subreddit validation in normalizeSubreddit
2026-03-05 20:07:29 -08:00
Waleed Latif
ff4b2f8c6a lint 2026-03-05 19:38:51 -08:00
Waleed Latif
4735245c8f feat(tables): inline cell editing with optimistic updates 2026-03-05 19:37:06 -08:00
Waleed
aac9e74283 feat(knowledge): add 10 new knowledge base connectors (#3430)
* feat(knowledge): add 10 new knowledge base connectors

Add connectors for Dropbox, OneDrive, SharePoint, Slack, Google Docs,
Asana, HubSpot, Salesforce, WordPress, and Webflow. Each connector
implements listDocuments, getDocument, validateConfig with proper
pagination, content hashing, and tag definitions.

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

* fix(connectors): address audit findings across 5 connectors

OneDrive: fix encodeURIComponent breaking folder paths with slashes,
add recursive folder traversal via folder queue in cursor state.
Slack: add missing requiredScopes.
Asana: pass retryOptions as 3rd arg to fetchWithRetry instead of
spreading into RequestInit; add missing requiredScopes.
HubSpot: add missing requiredScopes; fix sort property to use
hs_lastmodifieddate for non-contact object types.
Google Docs: remove orphaned title tag that was never populated.

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

* fix(connectors): add missing requiredScopes to OneDrive and HubSpot

OneDrive: add requiredScopes: ['Files.Read']
HubSpot: add missing crm.objects.tickets.read scope

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

* chore(connectors): lint fixes

* fix(connectors): slice documents to respect max limit on last page

* fix(connectors): use per-segment encodeURIComponent for SharePoint folder paths

encodeURI does not encode #, ?, &, + or = which are valid in folder
names but break the Microsoft Graph URL. Apply the same per-segment
encoding fix already used in the OneDrive connector.

* fix(connectors): address PR review findings

- Slack: remove private_channel from conversations.list types param
  since requiredScopes only cover public channels (channels:read,
  channels:history). Adding groups:read/groups:history would force
  all users to grant private channel access unnecessarily.
- OneDrive/SharePoint: add .htm to supported extensions and handle
  it in content processing (htmlToPlainText), matching Dropbox.
- Salesforce: guard getDocument for KnowledgeArticleVersion to skip
  records that are no longer PublishStatus='Online', preventing
  un-published articles from being re-synced.

* fix(connectors): pre-download size check and remove dead parameter

- OneDrive/SharePoint: add file size check against MAX_FILE_SIZE before
  downloading, matching Dropbox's behavior. Prevents OOM on large files.
- Slack: remove unused syncContext parameter from fetchChannelMessages.

* fix(connectors): slack getDocument user cache & wordpress scope reduction

- Slack: pass a local syncContext to formatMessages in getDocument so
  resolveUserName caches user lookups across messages. Without this,
  every message triggered a fresh users.info API call.
- WordPress: replace 'global' scope with 'posts' and 'sites' following
  principle of least privilege. The connector only reads posts and
  validates site existence.

* fix(connectors): revert wordpress scope and slack local cache changes

- WordPress: revert requiredScopes to ['global'] — the scope check
  does literal string matching, so ['posts', 'sites'] would always
  fail since auth.ts requests 'global' from WordPress.com OAuth.
  Reducing scope requires changing both auth.ts and the connector.
- Slack: remove local syncContext from getDocument — the perf impact
  of uncached users.info calls is negligible for typical channels
  (bounded by unique users, not message count).

* fix(connectors): align requiredScopes with auth.ts registrations

The scope check in getMissingRequiredScopes does literal string matching
against the OAuth token's granted scopes. requiredScopes must match what
auth.ts actually requests (since that's what the provider returns).

- HubSpot: use 'tickets' (legacy scope in auth.ts) instead of
  'crm.objects.tickets.read' (v3 granular scope not requested)
- Google Docs: use 'drive' (what auth.ts requests) instead of
  'documents.readonly' and 'drive.readonly' (never requested,
  so never in the granted set)

* fix(connectors): align Google Drive requiredScopes with auth.ts

Google Drive connector required 'drive.readonly' but auth.ts requests
'drive' (the superset). Since scope validation does literal matching,
this caused a spurious 'Additional permissions required' warning.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 19:31:17 -08:00
Siddharth Ganesan
d6b97fee08 Fix lint 2026-03-05 17:37:41 -08:00
Siddharth Ganesan
280ac30d55 Jobs 2026-03-05 17:36:24 -08:00
Siddharth Ganesan
5c24d2422e Jobs 2026-03-05 17:35:38 -08:00
Siddharth Ganesan
9d001eaf70 Jobs 2026-03-05 17:32:36 -08:00
Vikhyath Mondreti
3ce947566d v0.5.106: condition block and legacy kbs fixes, GPT 5.4 2026-03-05 17:30:05 -08:00
Vikhyath Mondreti
4f45f705a5 improvement(snapshot): exclude sentinel in client side activation detection (#3432) 2026-03-05 17:26:09 -08:00
Siddharth Ganesan
17e1bb5331 Nuke migrations 2026-03-05 17:22:40 -08:00
Vikhyath Mondreti
d640fa0852 fix(condition): execution with subflow sentinels follow-on, snapshot highlighting, duplicate terminal logs (#3429)
* fix(condition): consecutive error logging + execution dequeuing

* fix snapshot highlighting

* address minor gaps

* fix incomplete case

* remove activatedEdges path

* cleanup tests

* address greptile comments

* update tests:
2026-03-05 17:03:02 -08:00
Siddharth Ganesan
443e15eb01 Jobs 2026-03-05 16:43:48 -08:00
Waleed
dbef14ba26 feat(knowledge): connectors, user exclusions, expanded tools & airtable integration (#3230)
* feat(knowledge): connectors, user exclusions, expanded tools & airtable integration

* improvements

* removed redundant util

* ack PR comments

* remove module level cache, use syncContext between paginated calls to avoid redundant schema fetches

* regen migrations, ack PR comments

* ack PR comment

* added tests

* ack comments

* ack comments

* feat(db): add knowledge connector migration after merge

Generated migration 0162 for knowledge_connector and
knowledge_connector_sync_log tables after resolving merge
conflicts with feat/mothership-copilot.

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

* fix(connectors): audit fixes for sync engine, connectors, and knowledge tools

- Extract shared computeContentHash to connectors/utils.ts (dedup across 7 connectors)
- Include error'd connectors in cron auto-retry query
- Add syncContext caching for Confluence (cloudId, spaceId)
- Batch Confluence label fetches with concurrency limit of 10
- Enforce maxPages in Confluence v2 path
- Clean up stale storage files on document update
- Retry stuck documents (pending/failed) after sync completes
- Soft-delete documents and reclaim tag slots on connector deletion
- Add incremental sync support to ConnectorConfig interface
- Fix offset:0 falsy check in list_documents tool

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

* perf(connectors): deep audit — extract shared utils, fix pagination, optimize API calls

- Extract shared htmlToPlainText to connectors/utils.ts (dedup Confluence + Google Drive)
- Add syncContext caching for Jira cloudId, Notion/Linear/Google Drive cumulative limits
- Fix cumulative maxPages/maxIssues/maxFiles enforcement across pagination pages
- Bump Notion page_size from 20 to 100 (5x fewer API round-trips)
- Batch Notion child page fetching with concurrency=5 (was serial N+1)
- Bump Confluence v2 limit from 50 to 250 (v2 API supports it)
- Pass syncContext through Confluence CQL path for cumulative tracking
- Upgrade GitHub tree truncation warning to error level
- Fix sync-engine test mock to include inArray export

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

* refactor(connectors): extract tag helpers, fix Notion maxPages, rewrite broken tests

- Add parseTagDate and joinTagArray helpers to connectors/utils.ts
- Update all 7 connectors to use shared tag mapping helpers (removes 12+ duplication instances)
- Fix Notion listFromParentPage cumulative maxPages check (was using local count)
- Rewrite 3 broken connector route test files to use vi.hoisted() + static vi.mock()
  pattern instead of deprecated vi.doMock/vi.resetModules (all 86 tests now pass)

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

* fix(connectors): add loading skeletons, delete pending state, and pause feedback

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

* fix(knowledge): escape LIKE wildcards, guard restore from un-deleting, fix offset=0

- Escape %, _, \ in tag filter LIKE patterns to prevent incorrect matches
- Add isNull(deletedAt) guard to restore operation to prevent un-deleting soft-deleted docs
- Change offset check from falsy to != null so offset=0 is not dropped

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:40:00 -08:00
Siddharth Ganesan
7140867ff9 Jobs 2026-03-05 15:14:45 -08:00
Siddharth Ganesan
73cd10ca21 Jobs 2 2026-03-05 14:40:49 -08:00
Waleed
a368827f1e feat(api): add tables and files v1 REST API with OpenAPI docs (#3422)
* feat(api): add tables and files v1 REST API with OpenAPI docs

* fix(api): address review feedback for tables/files REST API

* fix(api): reject empty filters, consolidate PUT/DELETE into service helpers

* fix(api): upsert unique constraints, POST response fields, uploadedAt timestamp

* fix(api): stop leaking internal fields in list tables, fix deleteTable requestId

* fix(api): atomic table-count limit in createTable, stop leaking internal fields

* fix(api): error classification in PATCH, z.coerce→preprocess, requestId in logs

* fix(api): audit logging, PATCH service consolidation, Content-Disposition encoding

- Add TABLE_CREATED/TABLE_DELETED audit events to v1 table routes
- Consolidate PATCH handlers to use updateRow service function
- Fix Content-Disposition header with RFC 5987 dual-parameter form
- Normalize schema in POST /tables response with normalizeColumn

* lint

* fix(api): upsert unique constraint 400, guard request.json() parse errors

- Add 'Unique constraint violation' to upsert error classification
- Wrap PUT/DELETE request.json() in try/catch to return 400 on malformed body
- Apply fixes to both v1 and internal routes

* fix(api): guard PATCH request.json(), accurate deleteRowsByIds count

- Wrap PATCH request.json() in try/catch for both v1 and internal routes
- Rewrite deleteRowsByIds to use .returning() for accurate deletedCount
  under concurrent requests (eliminates SELECT-then-DELETE race)

* fix(api): guard all remaining request.json() calls in table routes

- Wrap POST handler request.json() in try/catch across all table routes
- Also fix internal DELETE single-row handler
- Every request.json() in table routes now returns 400 on malformed body

* fix(api): safe type check on formData workspaceId in file upload

- Replace unsafe `as string | null` cast with typeof check
- Prevents File object from bypassing workspaceId validation

* fix(api): safe File cast in upload, validate column name before sql.raw()

- Use instanceof File check instead of unsafe `as File | null` cast
- Add regex validation on column name before sql.raw() interpolation

* fix(api): comprehensive hardening pass across all table/file routes

- Guard request.formData() with try/catch in file upload
- Guard all .toISOString() calls with instanceof Date checks
- Replace verifyTableWorkspace double-fetch with direct comparison
- Fix relative imports to absolute (@/app/api/table/utils)
- Fix internal list tables leaking fields via ...t spread
- Normalize schema in internal POST create table response
- Remove redundant pre-check in internal create (service handles atomically)
- Make 'maximum table limit' return 403 consistently (was 400 in internal)
- Add 'Row not found' → 404 classification in PATCH handlers
- Add NAME_PATTERN validation before sql.raw() in validation.ts

* chore: lint fixes
2026-03-05 13:16:13 -08:00
Vikhyath Mondreti
28f8e0fd97 fix(kbs): legacy subblock id migration + CI check (#3425)
* fix(kbs): legacy subblock id migration + CI check

* cleanup migration code

* address regex inaccuracy
2026-03-05 12:38:12 -08:00
Waleed
cc38ecaf12 feat(models): add gpt-5.4 and gpt-5.4-pro model definitions (#3424)
* feat(models): add gpt-5.4 and gpt-5.4-pro model definitions

* fix(providers): update test for gpt-5.4-pro missing verbosity support
2026-03-05 11:59:52 -08:00
Siddharth Ganesan
eac8aca0c0 Schedules page for workflows 2026-03-05 10:31:01 -08:00
Waleed
70c36cb7aa v0.5.105: slack remove reaction, nested subflow locks fix, servicenow pagination, memory improvements 2026-03-04 22:38:26 -08:00
Waleed
0a6a2ee694 feat(slack): add new tools and user selectors (#3420)
* feat(slack): add new tools and user selectors

* fix(slack): fix download fileName param and canvas error handling

* fix(slack): use markdown format for canvas rename title_content

* fix(slack): rename channel output to channelInfo and document presence API limitation

* lint

* fix(chat): use explicit trigger type check instead of heuristic for chat guard (#3419)

* fix(chat): use explicit trigger type check instead of heuristic for chat guard

* fix(chat): remove heuristic fallback from isExecutingFromChat

Use only overrideTriggerType === 'chat' instead of also checking
for 'input' in workflowInput, which can false-positive on manual
executions with workflow input.

* fix(chat): use isExecutingFromChat variable consistently in callbacks

Replace inline overrideTriggerType !== 'chat' checks with
!isExecutingFromChat to stay consistent with the rest of the function.

* fix(slack): add missing fields to SlackChannel interface

* fix(slack): fix canvas transformResponse type mismatch

Provide required output fields on error path to match SlackCanvasResponse type.

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

* fix(slack): move error field to top level in canvas transformResponse

The error field belongs on ToolResponse, not inside the output object.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:28:10 -08:00
Waleed
8579beb199 fix(chat): use explicit trigger type check instead of heuristic for chat guard (#3419)
* fix(chat): use explicit trigger type check instead of heuristic for chat guard

* fix(chat): remove heuristic fallback from isExecutingFromChat

Use only overrideTriggerType === 'chat' instead of also checking
for 'input' in workflowInput, which can false-positive on manual
executions with workflow input.

* fix(chat): use isExecutingFromChat variable consistently in callbacks

Replace inline overrideTriggerType !== 'chat' checks with
!isExecutingFromChat to stay consistent with the rest of the function.
2026-03-04 19:05:45 -08:00
Waleed
115b4581a5 fix(editor): pass workspaceId to useCredentialName in block preview (#3418) 2026-03-04 18:15:27 -08:00
Waleed
fcdcaed00d fix(memory): add Bun.gc, stream cancellation, and unconsumed fetch drains (#3416)
* fix(memory): add Bun.gc, stream cancellation, and unconsumed fetch drains

* fix(memory): await reader.cancel() and use non-blocking Bun.gc

* fix(memory): update Bun.gc comment to match non-blocking call

* fix(memory): use response.body.cancel() instead of response.text() for drains

* fix(executor): flush TextDecoder after streaming loop for multi-byte chars

* fix(memory): use text() drain for SecureFetchResponse which lacks body property

* fix(chat): prevent premature isExecuting=false from killing chat stream

The onExecutionCompleted/Error/Cancelled callbacks were setting
isExecuting=false as soon as the server-side SSE stream completed.
For chat executions, this triggered a useEffect in chat.tsx that
cancelled the client-side stream reader before it finished consuming
buffered data — causing empty or partial chat responses.

Skip the isExecuting=false in these callbacks for chat executions
since the chat's own finally block handles cleanup after the stream
is fully consumed.

* fix(chat): remove useEffect anti-pattern that killed chat stream on state change

The effect reacted to isExecuting becoming false to clean up streams,
but this is an anti-pattern per React guidelines — using state changes
as a proxy for events. All cleanup cases are already handled by proper
event paths: stream done (processStreamingResponse), user cancel
(handleStopStreaming), component unmount (cleanup effect), and
abort/error (catch block).

* fix(servicenow): remove invalid string comparison on numeric offset param

* upgrade turborepo
2026-03-04 17:46:20 -08:00
Siddharth Ganesan
337154054e Oauth link 2026-03-04 17:35:32 -08:00
Waleed
04fa31864b feat(servicenow): add offset and display value params to read records (#3415)
* feat(servicenow): add offset and display value params to read records

* fix(servicenow): address greptile review feedback for offset and displayValue

* fix(servicenow): handle offset=0 correctly in pagination

* fix(servicenow): guard offset against empty string in URL builder
2026-03-04 17:01:31 -08:00
Siddharth Ganesan
c6ac0b4445 Agent subdir 2026-03-04 16:50:24 -08:00
Waleed
6b355e9b54 fix(subflows): recurse into all descendants for lock, enable, and protection checks (#3412)
* fix(subflows): recurse into all descendants for lock, enable, and protection checks

* fix(subflows): prevent container resize on initial render and clean up code

- Add canvasReadyRef to skip container dimension recalculation during
  ReactFlow init — position changes from extent clamping fired before
  block heights are measured, causing containers to resize on page load
- Resolve globals.css merge conflict, remove global z-index overrides
  (handled via ReactFlow zIndex prop instead)
- Clean up subflow-node: hoist static helpers to module scope, remove
  unused ref, fix nested ternary readability, rename outlineColor→ringColor

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

* fix(subflows): use full ancestor-chain protection for descendant enable-toggle

The enable-toggle for descendants was checking only direct `locked` status
instead of walking the full ancestor chain via `isBlockProtected`. This meant
a block nested 2+ levels inside a locked subflow could still be toggled.
Also added TSDoc clarifying why boxShadow works for subflow ring indicators.

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

* revert(subflows): remove canvasReadyRef height-gating approach

The canvasReadyRef gating in onNodesChange didn't fully fix the
container resize-on-load issue. Reverting to address properly later.

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

* fix: remove unintentional edge-interaction CSS from globals

Leftover from merge conflict resolution — not part of this PR's changes.

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

* fix(editor): correct isAncestorLocked when block and ancestor both locked, restore fade-in transition

isAncestorLocked was derived from isBlockProtected which short-circuits
on block.locked, so a self-locked block inside a locked ancestor showed
"Unlock block" instead of "Ancestor container is locked". Now walks the
ancestor chain independently.

Also restores the accidentally removed transition-opacity duration-150
class on the ReactFlow container.

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

* fix(subflows): use full ancestor-chain protection for top-level enable-toggle, restore edge-label z-index

The top-level block check in batchToggleEnabled used block.locked (self
only) while descendants used isBlockProtected (full ancestor chain). A
block inside a locked ancestor but not itself locked would bypass the
check. Now all three layers (store, collaborative hook, DB operations)
consistently use isBlockProtected/isDbBlockProtected at both levels.

Also restores the accidentally removed edge-labels z-index rule, bumped
from 60 to 1001 so labels render above child nodes (zIndex: 1000).

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

* fix(subflows): extract isAncestorProtected utility, add cycle detection to all traversals

- Extract isAncestorProtected from utils.ts so editor.tsx doesn't
  duplicate the ancestor-chain walk. isBlockProtected now delegates to it.
- Add visited-set cycle detection to all ancestor walks
  (isBlockProtected, isAncestorProtected, isDbBlockProtected) and
  descendant searches (findAllDescendantNodes, findDbDescendants) to
  guard against corrupt parentId references.
- Document why click-catching div has no event bubbling concern
  (ReactFlow renders children as viewport siblings, not DOM children).

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:51:32 -08:00
Waleed
127994f077 feat(slack): add remove reaction tool (#3414)
* feat(slack): add remove reaction tool

* lint
2026-03-04 15:28:41 -08:00
Waleed
b07925fcc0 feat(settings): migrate settings from modal to route-based pages (#3413) 2026-03-04 15:20:52 -08:00
Siddharth Ganesan
08fb8c1651 Tool perms 2026-03-04 13:44:46 -08:00
Siddharth Ganesan
37337aece5 Scope perms 2026-03-04 12:44:04 -08:00
Siddharth Ganesan
da349176ab Fix merge conflicts 2026-03-04 11:17:01 -08:00
Siddharth Ganesan
6f3559ce8f Fix merge conflicts 2026-03-04 11:15:43 -08:00
Siddharth Ganesan
9a7b5ffe64 Fix merge conflicts 2026-03-04 11:13:42 -08:00
Siddharth Ganesan
4ede071ecb Fix merge conflicts 2026-03-04 11:12:51 -08:00
Siddharth Ganesan
161fb37244 Remove migrations 2026-03-04 10:48:12 -08:00
Emir Karabeg
d1575927a2 improvement(theme): system default 2026-03-04 01:29:47 -08:00
Waleed
f1ec5fe824 v0.5.104: memory improvements, nested subflows, careers page redirect, brandfetch, google meet 2026-03-03 23:45:29 -08:00
Waleed
efc1aeed70 fix(subflows): fix pointer events for nested subflow interaction (#3409)
* fix(subflows): fix pointer events for nested subflow interaction

* fix(subflows): use Tailwind class for pointer-events-none
2026-03-03 23:28:51 -08:00
Emir Karabeg
a3b19fb32a improvement(user-input): ui, files 2026-03-03 22:56:07 -08:00
Waleed
46065983f6 fix(editor): restore cursor position after tag/env-var completion in code editors (#3406)
* fix(editor): restore cursor position after tag/env-var completion in code editors

* lint

* refactor(editor): extract restoreCursorAfterInsertion helper, fix weak fallbacks

* updated

* fix(editor): replace useEffect with direct ref assignment for editorValueRef

* fix(editor): guard cursor restoration behind preview/readOnly check

Move restoreCursorAfterInsertion inside the !isPreview && !readOnly guard
so cursor position isn't computed against newValue when the textarea still
holds liveValue. Add comment documenting the cross-string index invariant
in the shared helper.

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

* fix(editor): escape blockId in CSS selector with CSS.escape()

Prevents potential SyntaxError if blockId ever contains CSS special
characters when querying the textarea for cursor restoration.

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

* perf(editor): use ref for cursor fallback to stabilize useCallback

Replace cursorPosition state in handleSubflowTagSelect's dependency
array with a cursorPositionRef. This avoids recreating the callback
on every keystroke since cursorPosition is only used as a fallback
when textareaRef.current is null.

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

* refactor(editor): pass cursor position explicitly from dropdowns

Instead of inferring cursor position by searching for delimiters in the
output string (which could match unrelated < or {{ in code), compute
the exact cursor position in TagDropdown and EnvVarDropdown where the
insertion range is definitively known, and pass it through onSelect.

This follows the same pattern used by CodeMirror, Monaco, and
ProseMirror: the insertion source always knows the range, so cursor
position is computed at the source rather than inferred by the consumer.

- TagDropdown/EnvVarDropdown: compute newCursorPosition, pass as 2nd arg
- restoreCursorAfterInsertion: simplified to just (textarea, position)
- code.tsx, condition-input.tsx, use-subflow-editor.ts: accept position
- Removed editorValueRef and cursorPositionRef from use-subflow-editor
  (no longer needed since dropdown computes position)
- Other consumers (native inputs) unaffected due to TS callback compat

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

* docs(editor): fix JSDoc terminology — macrotask not microtask

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 22:10:00 -08:00
Waleed
2c79d0249f improvement(executor): support nested loops/parallels (#3398)
* feat(executor): support nested loop DAG construction and edge wiring

Wire inner loop sentinel nodes into outer loop sentinel chains so that
nested loops execute correctly. Resolves boundary-node detection to use
effective sentinel IDs for nested loops, handles loop-exit edges from
inner sentinel-end to outer sentinel-end, and recursively clears
execution state for all nested loop scopes between iterations.

NOTE: loop-in-loop nesting only; parallel nesting is not yet supported.
Made-with: Cursor

* feat(executor): add nested loop iteration context and named loop variable resolution

Introduce ParentIteration to track ancestor loop state, build a
loopParentMap during DAG construction, and propagate parent iterations
through block execution and child workflow contexts.

Extend LoopResolver to support named loop references (e.g. <loop1.index>)
and add output property resolution (<loop1.result>). Named references
use the block's display name normalized to a tag-safe identifier,
enabling blocks inside nested loops to reference any ancestor loop's
iteration state.

NOTE: loop-in-loop nesting only; parallel nesting is not yet supported.
Made-with: Cursor

* feat(terminal): propagate parent iteration context through SSE events and terminal display

Thread parentIterations through SSE block-started, block-completed, and
block-error events so the terminal can reconstruct nested loop
hierarchies. Update the entry tree builder to recursively nest inner
loop subflow nodes inside their parent iteration rows, using
parentIterations depth-stripping to support arbitrary nesting depth.

Display the block's store name for subflow container rows instead of
the generic "Loop" / "Parallel" label.

Made-with: Cursor

* feat(canvas): allow nesting subflow containers and prevent cycles

Remove the restriction that prevented subflow nodes from being dragged
into other subflow containers, enabling loop-in-loop nesting on the
canvas. Add cycle detection (isDescendantOf) to prevent a container
from being placed inside one of its own descendants.

Resize all ancestor containers when a nested child moves, collect
descendant blocks when removing from a subflow so boundary edges are
attributed correctly, and surface all ancestor loop tags in the tag
dropdown for blocks inside nested loops.

Made-with: Cursor

* feat(agent): add MCP server discovery mode for agent tool input (#3353)

* feat(agent): add MCP server discovery mode for agent tool input

* fix(tool-input): use type variant for MCP server tool count badge

* fix(mcp-dynamic-args): align label styling with standard subblock labels

* standardized inp format UI

* feat(tool-input): replace MCP server inline expand with drill-down navigation

* feat(tool-input): add chevron affordance and keyboard nav for MCP server drill-down

* fix(tool-input): handle mcp-server type in refresh, validation, badges, and usage control

* refactor(tool-validation): extract getMcpServerIssue, remove fake tool hack

* lint

* reorder dropdown

* perf(agent): parallelize MCP server tool creation with Promise.all

* fix(combobox): preserve cursor movement in search input, reset query on drilldown

* fix(combobox): route ArrowRight through handleSelect, remove redundant type guards

* fix(agent): rename mcpServers to mcpServerSelections to avoid shadowing DB import, route ArrowRight through handleSelect

* docs: update google integration docs

* fix(tool-input): reset drilldown state on tool selection to prevent stale view

* perf(agent): parallelize MCP server discovery across multiple servers

* improvement(tests): speed up unit tests by eliminating vi.resetModules anti-pattern (#3357)

* improvement(tests): speed up unit tests by eliminating vi.resetModules anti-pattern

- convert 51 test files from vi.resetModules/vi.doMock/dynamic import to vi.hoisted/vi.mock/static import
- add global @sim/db mock to vitest.setup.ts
- switch 4 test files from jsdom to node environment
- remove all vi.importActual calls that loaded heavy modules (200+ block files)
- remove slow mockConsoleLogger/mockAuth/setupCommonApiMocks helpers
- reduce real setTimeout delays in engine tests
- mock heavy transitive deps in diff-engine test

test execution time: 34s -> 9s (3.9x faster)
environment time: 2.5s -> 0.6s (4x faster)

* docs(testing): update testing best practices with performance rules

- document vi.hoisted + vi.mock + static import as the standard pattern
- explicitly ban vi.resetModules, vi.doMock, vi.importActual, mockAuth, setupCommonApiMocks
- document global mocks from vitest.setup.ts
- add mock pattern reference for auth, hybrid auth, and database chains
- add performance rules section covering heavy deps, jsdom vs node, real timers

* fix(tests): fix 4 failing test files with missing mocks

- socket/middleware/permissions: add vi.mock for @/lib/auth to prevent transitive getBaseUrl() call
- workflow-handler: add vi.mock for @/executor/utils/http matching executor mock pattern
- evaluator-handler: add db.query.account mock structure before vi.spyOn
- router-handler: same db.query.account fix as evaluator

* fix(tests): replace banned Function type with explicit callback signature

* feat(databricks): add Databricks integration with 8 tools (#3361)

* feat(databricks): add Databricks integration with 8 tools

Add complete Databricks integration supporting SQL execution, job management,
run monitoring, and cluster listing via Personal Access Token authentication.

Tools: execute_sql, list_jobs, run_job, get_run, list_runs, cancel_run,
get_run_output, list_clusters

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

* fix(databricks): throw on invalid JSON params, fix boolean coercion, add expandTasks field

- Throw errors on invalid JSON in jobParameters/notebookParams instead of silently defaulting to {}
- Always set boolean params explicitly to prevent string 'false' being truthy
- Add missing expandTasks dropdown UI field for list_jobs operation

* fix(databricks): align tool inputs/outputs with official API spec

- execute_sql: fix wait_timeout default description (50s, not 10s)
- get_run: add queueDuration field, update lifecycle/result state enums
- get_run_output: fix notebook output size (5 MB not 1 MB), add logsTruncated field
- list_runs: add userCancelledOrTimedout to state, fix limit range (1-24), update state enums
- list_jobs: fix name filter description to "exact case-insensitive"
- list_clusters: add PIPELINE_MAINTENANCE to ClusterSource enum

* fix(databricks): regenerate docs to reflect API spec fixes

---------

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

* feat(luma): add Luma integration for event and guest management (#3364)

* feat(luma): add Luma integration for event and guest management

Add complete Luma (lu.ma) integration with 6 tools: get event, create event,
update event, list calendar events, get guests, and add guests. Includes block
configuration with wandConfig for timestamps/timezones/durations, advanced mode
for optional fields, and generated documentation.

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

* fix(luma): address PR review feedback

- Remove hosts field from list_events transformResponse (not in LumaEventEntry type)
- Fix truncated add_guests description by removing quotes that broke docs generator

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

* fix(luma): fix update_event field name and add_guests response parsing

- Use 'id' instead of 'event_id' in update_event request body per API spec
- Fix add_guests to parse entries[].guest response structure instead of flat guests array

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

---------

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

* feat(gamma): add gamma integration for AI-powered content generation (#3358)

* feat(gamma): add gamma integration for AI-powered content generation

* fix(gamma): address PR review comments

- Make credits/error conditionally included in check_status response to avoid always-truthy objects
- Replace full wordmark SVG with square "G" letterform for proper rendering in icon slots

* fix(gamma): remove imageSource from generate_from_template endpoint

The from-template API only accepts imageOptions.model and imageOptions.style,
not imageOptions.source (image source is inherited from the template).

* fix(gamma): use typed output in check_status transformResponse

* regen docs

* feat(greenhouse): add greenhouse integration for managing candidates, jobs, and applications (#3363)

* feat(ashby): add ashby integration for candidate, job, and application management (#3362)

* feat(ashby): add ashby integration for candidate, job, and application management

* fix(ashby): auto-fix lint formatting in docs files

* improvement(oauth): reordered oauth modal (#3368)

* feat(loops): add Loops email platform integration (#3359)

* feat(loops): add Loops email platform integration

Add complete Loops integration with 10 tools covering all API endpoints:
- Contact management: create, update, find, delete
- Email: send transactional emails with attachments
- Events: trigger automated email sequences
- Lists: list mailing lists and transactional email templates
- Properties: create and list contact properties

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

* ran litn

---------

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

* feat(resend): expand integration with contacts, domains, and enhanced email ops (#3366)

* improvement(blocks): update luma styling and linkup field modes (#3370)

* improvement(blocks): update luma styling and linkup field modes

* improvement(fireflies): move optional fields to advanced mode

* improvement(blocks): move optional fields to advanced mode for 10 integrations

* improvement(blocks): move optional fields to advanced mode for 6 more integrations

* feat(x): add 28 new X API v2 tool integrations and expand OAuth scopes (#3365)

* feat(x): add 28 new X API v2 tool integrations and expand OAuth scopes

* fix(x): add missing nextToken param to search tweets and fix XCreateTweetParams type

* fix(x): correct API spec issues in retweeted_by, quote_tweets, personalized_trends, and usage tools

* fix(x): add missing newestId and oldestId to error meta in get_liked_tweets and get_quote_tweets

* fix(x): add missing newestId/oldestId to get_liked_tweets success branch and includes to XTweetListResponse

* fix(x): add error handling to create_tweet and delete_tweet transformResponse

* fix(x): add error handling and logger to all X tools

* fix(x): revert block requiredScopes to match current operations

* feat(x): update block to support all 28 new X API v2 tools

* fix(x): add missing text output and fix hiddenResult output key mismatch

* docs(x): regenerate docs for all 28 new X API v2 tools

* improvement(docs): audit and standardize tool description sections, update developer count to 70k (#3371)

* improvement(x): align OAuth scopes, add scope descriptions, and set optional fields to advanced mode (#3372)

* improvement(x): align OAuth scopes, add scope descriptions, and set optional fields to advanced mode

* improvement(skills): add typed JSON outputs guidance to add-tools, add-block, and add-integration skills

* improvement(skills): add final validation steps to add-tools, add-block, and add-integration skills

* fix(skills): correct misleading JSON array comment in wandConfig example

* feat(skills): add validate-integration skill for auditing tools, blocks, and registry against API docs

* improvement(skills): expand validate-integration with full block-tool alignment, OAuth scopes, pagination, and error handling checks

* improvement(ci): add sticky disk caches and bump runner for faster builds (#3373)

* improvement(selectors): make selectorKeys declarative (#3374)

* fix(webflow): resolution for selectors

* remove unecessary fallback'

* fix teams selector resolution

* make selector keys declarative

* selectors fixes

* improvement(selectors): consolidate selector input logic (#3375)

* feat(google-contacts): add google contacts integration (#3340)

* feat(google-contacts): add google contacts integration

* fix(google-contacts): throw error when no update fields provided

* lint

* update icon

* improvement(google-contacts): add advanced mode, error handling, and input trimming

- Set mode: 'advanced' on optional fields (emailType, phoneType, notes, pageSize, pageToken, sortOrder)
- Add createLogger and response.ok error handling to all 6 tools
- Add .trim() on resourceName in get, update, delete URL builders

* improvement(mcp): add all MCP server tools individually instead of as single server entry (#3376)

* improvement(mcp): add all MCP server tools individually instead of as single server entry

* fix(mcp): prevent remove popover from opening inadvertently

* fix(sse): fix memory leaks in SSE stream cleanup and add memory telemetry (#3378)

* fix(sse): fix memory leaks in SSE stream cleanup and add memory telemetry

* improvement(monitoring): add SSE metering to wand, execution-stream, and a2a-message endpoints

* fix(workflow-execute): remove abort from cancel() to preserve run-on-leave behavior

* improvement(monitoring): use stable process.getActiveResourcesInfo() API

* refactor(a2a): hoist resubscribe cleanup to eliminate duplication between start() and cancel()

* style(a2a): format import line

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

* fix(wand): set guard flag on early-return decrement for consistency

---------

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

* improvement(ashby): validate ashby integration and update skill files (#3381)

* improvement(luma): expand host response fields and harden event ID inputs (#3383)

* improvement(resend): add error handling, authMode, and naming consistency (#3382)

* fix(chat-deploy): fix launch chat popup and auth persistence, clean up React anti-patterns (#3380)

* fix(chat-deploy): fix launch chat popup and auth persistence, clean up React anti-patterns

* lint

* fix(greenhouse): fix email_address query param, add .trim() to ID paths, revert onValidationChange to useEffect

* fix(chat-deploy): fix stale AuthSelector state, stabilize refetch ref, clean up copy timeout

* fix(chat-deploy): reset chatSuccess on modal open to prevent stuck state

* improvement(loops): validate loops integration and update skill files (#3384)

* improvement(loops): validate loops integration and update skill files

* loops icon color

* update databricks icon

* fix(monitoring): set MemoryTelemetry logger to INFO level for production visibility (#3386)

Production defaults to ERROR-only logging. Without this override,
memory snapshots would be silently suppressed.

* feat(integrations): add amplitude, google pagespeed insights, and pagerduty integrations (#3385)

* feat(integrations): add amplitude and google pagespeed insights integrations

* verified and regen docs

* fix icons

* fix(integrations): add pagerduty to tool and block registries

Re-add registry entries that were reverted after initial commit.

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

* more updates

* ack comemnts

---------

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

* feat(docs): add API reference with OpenAPI spec and auto-generated endpoint pages (#3388)

* feat(docs): add API reference with OpenAPI spec and auto-generated endpoint pages

* multiline curl

* random improvements

* cleanup

* update docs copy

* fix build

* cast

* fix builg

---------

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>

* fix(icons): fix pagerduty icon (#3392)

* improvement(executor): audit and harden nested loop/parallel implementation

* improvement(executor): audit and harden nested loop/parallel implementation

- Replace unsafe _childWorkflowInstanceId cast with typeof type guard
- Reuse WorkflowNodeMetadata interface instead of inline type duplication
- Rename _executeCore to executeCore (private, no underscore needed)
- Add log warning when SSE callbacks are dropped beyond MAX_SSE_CHILD_DEPTH
- Remove unnecessary onStream type assertion, use StreamingExecution type
- Convert OUTPUT_PROPERTIES/KNOWN_PROPERTIES from arrays to Sets for O(1) lookup
- Add type guard in loop resolver resolveOutput before casting
- Add TSDoc to edgeCrossesLoopBoundary explaining original-ID usage
- Add TSDoc to MAX_SSE_CHILD_DEPTH constant
- Update ParentIteration TSDoc to reflect parallel nesting support
- Type usageControl as union 'auto'|'force'|'none' in buildMcpTool
- Replace (t: any) casts with typed objects in agent-handler tests
- Add type guard in builder-data convertArrayItem
- Make ctx required in clearLoopExecutionState (only caller always passes it)
- Replace Math.random() with deterministic counter in terminal tests
- Fix isWorkflowBlockType mock to actually check block types
- Add loop-in-loop and workflow block tree tests

* improvement(executor): audit fixes for nested subflow implementation

- Fix findInnermostLoopForBlock/ParallelForBlock to return deepest nested
  container instead of first Object.keys() match
- Fix isBlockInLoopOrDescendant returning false when directLoopId equals
  target (should return true)
- Add isBlockInParallelOrDescendant with recursive nested parallel checking
  to match loop resolver behavior
- Extract duplicated ~20-line iteration context building from loop/parallel
  orchestrators into shared buildContainerIterationContext utility
- Remove inline import() type references in orchestrators
- Remove dead executionOrder field from WorkflowNodeMetadata
- Remove redundant double-normalization in findParallelBoundaryNodes
- Consolidate 3 identical tree-walk helpers into generic hasMatchInTree
- Add empty-array guards for Math.min/Math.max in terminal utils
- Make KNOWN_PROPERTIES a Set in parallel resolver for consistency
- Remove no-op handleDragEnd callback from toolbar
- Remove dead result/results entries from KNOWN_PROPERTIES in loop resolver
- Add tests for buildContainerIterationContext

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

* finished

* improvement(airtable): added more tools (#3396)

* fix(layout): polyfill crypto.randomUUID for non-secure HTTP contexts (#3397)

* feat(integrations): add dub.co integration (#3400)

* feat(integrations): add dub.co integration

* improvement(dub): add manual docs description and lint formatting fixes

* lint

* fix(dub): remove unsupported optional property from block outputs

* fix(memory): fix O(n²) string concatenation and unconsumed fetch response leaks (#3399)

* fix(monitoring): set MemoryTelemetry logger to INFO level for production visibility

Production defaults to ERROR-only logging. Without this override,
memory snapshots would be silently suppressed.

* fix(memory): fix O(n²) string concatenation and unconsumed fetch response leaks

* fix(tests): add text() mock to workflow-handler test fetch responses

* fix(memory): remove unused O(n²) join in onStreamChunk callback

* chore(careers): remove careers page, redirect to Ashby jobs portal (#3401)

* chore(careers): remove careers page, redirect to Ashby jobs portal

* lint

* feat(integrations): add google meet integration (#3403)

* feat(integrations): add google meet integration

* lint

* ack comments

* ack comments

* fix(terminal): deduplicate nested container entries in buildEntryTree

Filter out container-typed block rows when matching nested subflow
nodes exist, preventing nested loops/parallels from appearing twice
(once as a flat block and once as an expandable subflow).

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

* improvement(executor): clean up nested subflow implementation

- Fix wireSentinelEdges to use LOOP_EXIT handle for nested loop terminals
- Extract buildExecutionPipeline to deduplicate orchestrator wiring
- Replace two-phase init with constructor injection for Loop/ParallelOrchestrator
- Remove dead code: shouldExecuteLoopNode, resolveForEachItems, isLoopNode, isParallelNode, isSubflowBlockType
- Deduplicate currentItem resolution in ParallelResolver via resolveCurrentItem
- Type getDistributionItems param as SerializedParallel instead of any
- Demote verbose per-reference logger.info to logger.debug in evaluateWhileCondition
- Add loop-in-parallel wiring test in edges.test.ts

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

* fix(test): update parallel resolver test to use distribution instead of distributionItems

The distributionItems fallback was never part of SerializedParallel — it
only worked through any typing. Updated the test to use the real
distribution property.

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

* fix(executor): skip loop back-edges in parallel boundary detection and update test

findParallelBoundaryNodes now skips LOOP_CONTINUE back-edges when
detecting terminal nodes, matching findLoopBoundaryNodes behavior.
Without this, a nested loop's back-edge was incorrectly counted as a
forward edge within the parallel, preventing terminal detection.

Also updated parallel resolver test to use the real distribution
property instead of the non-existent distributionItems fallback.

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

* fix(executor): clean up cloned loop scopes in deleteParallelScopeAndClones

When a parallel contains a nested loop, cloned loop scopes (__obranch-N)
created by expandParallel were not being deleted, causing stale scopes to
persist across outer loop iterations.

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

* fix(executor): remove dead fallbacks, fix nested loop boundary detection, restore executionOrder

- Remove unreachable `?? candidateIds[0]` fallbacks in loop/parallel resolvers
- Remove arbitrary first-match fallback scan in findEffectiveContainerId
- Fix edgeCrossesLoopBoundary to use innermost loop detection for nested loops
- Add warning log for missing branch outputs in parallel aggregation
- Restore executionOrder on WorkflowNodeMetadata and pipe through child workflow notification
- Remove dead sim-drag-subflow classList.remove call
- Clean up cloned loop subflowParentMap entries in deleteParallelScopeAndClones

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

* leftover

* upgrade turborepo

* update stagehand icon

* fix(tag-dropdown): show contextual loop/parallel tags for deeply nested blocks

findAncestorLoops only checked direct loop membership, missing blocks nested
inside parallels within loops (and vice versa). Refactored to walk through
both loop and parallel containers recursively, so a block inside a parallel
inside a loop correctly sees the loop's contextual tags (index, currentItem)
instead of the loop's output tags (results).

Also fixed parallel ancestor detection to handle nested parallel-in-loop and
loop-in-parallel scenarios, collecting all ancestor parallels instead of just
the immediate containing one.

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

* testing

* fixed dedicated logs

* fix

* fix(subflows): enable nested subflow interaction and execution highlighting

Remove !important z-index overrides that prevented nested subflows from
being grabbed/dragged independently. Z-index is now managed by ReactFlow's
elevateNodesOnSelect and per-node zIndex: depth props. Also adds execution
status highlighting for nested subflows in both canvas and snapshot preview.

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

* fix(preview): add cycle guard to recursive subflow status derivation

Prevents infinite recursion if subflowChildrenMap contains circular
references by tracking visited nodes during traversal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.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: Vasyl Abramovych <vasyl.abramovych@gmail.com>
2026-03-03 19:21:52 -08:00
Waleed
1cf7fdfc8c fix(logs): add status field to log detail API for polling (#3405) 2026-03-03 18:00:21 -08:00
Emir Karabeg
21404d17e8 fix: message stream pickup and task ordering 2026-03-03 17:29:36 -08:00
Siddharth Ganesan
df7e731c9c Add payload 2026-03-03 16:15:01 -08:00
Emir Karabeg
4f4191fe1b fix: task ordering 2026-03-03 15:45:15 -08:00
Emir Karabeg
b57636e5b1 finalized task navigation 2026-03-03 15:24:31 -08:00
Emir Karabeg
38c9ecd259 resolved merge conflicts 2026-03-03 14:49:56 -08:00
Emir Karabeg
fadda6aaef improvement: task routing optimizations 2026-03-03 14:48:43 -08:00
Emir Karabeg
82f541e9de improvement: ui 2026-03-03 14:46:09 -08:00
Siddharth Ganesan
1339915957 Task vfs 2026-03-03 13:12:13 -08:00
Waleed
37bdffeda0 fix(socket): persist outbound edges from locked blocks (#3404)
* fix(socket): persist outbound edges from locked blocks

* fix(socket): align edge remove protection check with client-side behavior

* fix(socket): align batch edge protection checks with target-only model

* fix(socket): update stale comments for edge protection checks
2026-03-03 12:54:07 -08:00
Siddharth Ganesan
7fafc00a07 Task management 2026-03-03 12:00:03 -08:00
Emir Karabeg
fe5ab8aee8 improved streaming 2026-03-03 11:40:42 -08:00
Siddharth Ganesan
b3a639a693 Logs 2026-03-03 11:38:06 -08:00
Siddharth Ganesan
0249ca1480 Fix files 2026-03-03 10:49:59 -08:00
Siddharth Ganesan
553c376289 Fix routes 2026-03-03 10:23:11 -08:00
Waleed
6fa4b9b410 feat(integrations): add brandfetch integration (#3402)
* feat(integrations): add brandfetch integration

* lint

* ack comments
2026-03-02 22:10:38 -08:00
Waleed
f0ee492ada feat(integrations): add google meet integration (#3403)
* feat(integrations): add google meet integration

* lint

* ack comments
2026-03-02 21:59:09 -08:00
Emir Karabeg
4622966643 improvement(home): interactions 2026-03-02 17:25:32 -08:00
Siddharth Ganesan
e9550c624d Wand 2026-03-02 15:12:59 -08:00
Siddharth Ganesan
1d48289c53 Mothership block pudate 2026-03-02 15:05:56 -08:00
Siddharth Ganesan
fce10241a5 Mothership block 2026-03-02 14:55:04 -08:00
Waleed
a8e0203a92 chore(careers): remove careers page, redirect to Ashby jobs portal (#3401)
* chore(careers): remove careers page, redirect to Ashby jobs portal

* lint
2026-03-02 14:12:03 -08:00
Waleed
ebb9a2bdd3 fix(memory): fix O(n²) string concatenation and unconsumed fetch response leaks (#3399)
* fix(monitoring): set MemoryTelemetry logger to INFO level for production visibility

Production defaults to ERROR-only logging. Without this override,
memory snapshots would be silently suppressed.

* fix(memory): fix O(n²) string concatenation and unconsumed fetch response leaks

* fix(tests): add text() mock to workflow-handler test fetch responses

* fix(memory): remove unused O(n²) join in onStreamChunk callback
2026-03-02 13:58:03 -08:00
Waleed
61a447aba5 feat(integrations): add dub.co integration (#3400)
* feat(integrations): add dub.co integration

* improvement(dub): add manual docs description and lint formatting fixes

* lint

* fix(dub): remove unsupported optional property from block outputs
2026-03-02 13:45:09 -08:00
Emir Karabeg
ae080f125c Merge branch 'feat/landing' into feat/mothership-copilot 2026-03-02 13:44:12 -08:00
Emir Karabeg
0fb840c8fd Cleaned up home 2026-03-02 13:39:34 -08:00
Emir Karabeg
2c20519bbd improvement: ui/ux 2026-03-02 12:36:32 -08:00
Waleed
e91ab6260a fix(layout): polyfill crypto.randomUUID for non-secure HTTP contexts (#3397) 2026-03-02 11:57:31 -08:00
Siddharth Ganesan
f3474b0c90 Tool call loop 2026-03-02 11:15:17 -08:00
Waleed
afaa361801 improvement(airtable): added more tools (#3396) 2026-03-02 10:58:21 -08:00
Waleed
cd88706ea4 fix(icons): fix pagerduty icon (#3392) 2026-03-01 23:43:09 -08:00
Siddharth Ganesan
b2cc5b6738 Billing 2026-02-28 17:51:26 -08:00
Siddharth Ganesan
d49a2c1c25 Fixes 2026-02-27 15:56:04 -08:00
Siddharth Ganesan
8fa4745893 MCP commented out 2026-02-27 11:18:38 -08:00
Siddharth Ganesan
c168e36a05 Fix 2026-02-26 17:48:53 -08:00
Siddharth Ganesan
9cc46ffa43 Edit subagents 2026-02-26 15:53:58 -08:00
Siddharth Ganesan
cc5e592c46 Kb checkpoint 2026-02-26 14:59:56 -08:00
Siddharth Ganesan
7276136398 Piping 2026-02-26 12:32:09 -08:00
Siddharth Ganesan
3ad7af4b97 File creation 2026-02-25 19:23:24 -08:00
Siddharth Ganesan
3cb1768a44 Move files to separate resource 2026-02-25 18:33:07 -08:00
Siddharth Ganesan
11e6387a7d Fix run workflow 2026-02-25 18:12:13 -08:00
Siddharth Ganesan
57a91027de Fix condition edges 2026-02-25 17:48:33 -08:00
Emir Karabeg
49c29d5f7d feat: pricing, collaboration improvement, features skeleton 2026-02-25 16:28:56 -08:00
Emir Karabeg
843af915bc feat: integrations skeleton, realtime complete 2026-02-25 16:28:56 -08:00
Emir Karabeg
bb3e899f74 feat(landing): template, generic workflow 2026-02-25 16:28:56 -08:00
Emir Karabeg
e47dcdcc43 feat(landing): navbar, metadata, hero, templates header 2026-02-25 16:28:55 -08:00
Emir Karabeg
3e6cf24762 feat(landing): structure 2026-02-25 16:28:55 -08:00
Siddharth Ganesan
90a12546b2 Fix lint 2026-02-25 12:56:58 -08:00
Siddharth Ganesan
b6f8439267 Remove dead code 2026-02-25 12:55:50 -08:00
Siddharth Ganesan
4f74a8b845 Checkpopint 2026-02-25 12:45:55 -08:00
Siddharth Ganesan
f12d8f631f Split 2026-02-25 12:37:23 -08:00
Siddharth Ganesan
41f0957ccc Separation of route 2026-02-25 12:19:26 -08:00
Siddharth Ganesan
7b813be1dd Fix truncation 2026-02-25 11:09:04 -08:00
Siddharth Ganesan
704fa16bb4 run workflow checkpoint 2026-02-25 11:08:44 -08:00
Siddharth Ganesan
eccad2a8ce Remove dup code from tool calls 2026-02-24 16:59:40 -08:00
Siddharth Ganesan
87f5c464d9 Consolidation 2026-02-24 14:55:35 -08:00
Siddharth Ganesan
724aaa1432 table tools 2026-02-24 14:32:55 -08:00
Siddharth Ganesan
3de3ef4786 Readd migration 2026-02-24 14:03:30 -08:00
Siddharth Ganesan
743f048442 Merge with origin staging 2026-02-24 14:02:59 -08:00
Siddharth Ganesan
bbcf346df0 Nuke migration 2026-02-24 13:57:31 -08:00
Siddharth Ganesan
b9c3c2f78f Checkpoint interface consolidation 2026-02-24 13:55:50 -08:00
Siddharth Ganesan
d333307a17 Checkpoint 2026-02-24 13:47:29 -08:00
Siddharth Ganesan
134c4c4f2a Checkpoint 2026-02-24 12:22:19 -08:00
Siddharth Ganesan
03908edcbb Checkpoint 2026-02-19 14:47:57 -08:00
Siddharth Ganesan
3112485c31 Checkpoint 2026-02-19 11:08:32 -08:00
Siddharth Ganesan
459c2930ae Checkpoint 2026-02-19 10:14:24 -08:00
Siddharth Ganesan
3338b25c30 Checkpoint 2026-02-18 18:55:10 -08:00
Siddharth Ganesan
4c3002f97d Checkpoint 2026-02-18 18:38:37 -08:00
Siddharth Ganesan
632e0e0762 Checkpoitn 2026-02-18 15:29:58 -08:00
Siddharth Ganesan
7599774974 Checkpoint 2026-02-17 18:54:15 -08:00
Siddharth Ganesan
471e58a2d0 Checkpoint 2026-02-17 17:04:34 -08:00
Siddharth Ganesan
231ddc59a0 V0 2026-02-17 16:07:55 -08:00
Siddharth Ganesan
b197f68828 v0 2026-02-17 15:28:23 -08:00
1270 changed files with 187485 additions and 24985 deletions

View File

@@ -20,6 +20,7 @@ When the user asks you to create a block:
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
@@ -115,12 +116,17 @@ export const {ServiceName}Block: BlockConfig = {
id: 'credential',
title: 'Account',
type: 'oauth-input',
serviceId: '{service}', // Must match OAuth provider
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.)
@@ -624,6 +630,7 @@ export const registry: Record<string, BlockConfig> = {
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',
@@ -654,6 +661,7 @@ export const ServiceBlock: BlockConfig = {
title: 'Service Account',
type: 'oauth-input',
serviceId: 'service',
requiredScopes: getScopesForService('service'),
placeholder: 'Select account',
required: true,
},
@@ -792,7 +800,8 @@ All tool IDs referenced in `tools.access` and returned by `tools.config.tool` MU
- [ ] 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`
- [ ] 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

View File

@@ -0,0 +1,299 @@
---
description: Add a knowledge base connector for syncing documents from an external source
argument-hint: <service-name> [api-docs-url]
---
# 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) => {
// Paginate via cursor, extract text, compute SHA-256 hash
// Return { documents: ExternalDocument[], nextCursor?, hasMore }
},
getDocument: async (accessToken, sourceConfig, externalId) => {
// Return ExternalDocument 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.
```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' },
],
}
```
## 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
mimeType: 'text/plain' // Always text/plain (content is extracted)
contentHash: string // SHA-256 of content (change detection)
sourceUrl?: string // Link back to original (stored on document record)
metadata?: Record<string, unknown> // Source-specific data (fed to mapTags)
}
```
## Content Hashing (Required)
The sync engine uses content hashes for change detection:
```typescript
async function computeContentHash(content: string): Promise<string> {
const data = new TextEncoder().encode(content)
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('')
}
```
## 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**: `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
- [ ] `listDocuments` handles pagination and computes content hashes
- [ ] `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

@@ -114,6 +114,7 @@ export const {service}{Action}Tool: ToolConfig<Params, Response> = {
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}',
@@ -144,6 +145,7 @@ export const {Service}Block: BlockConfig = {
title: '{Service} Account',
type: 'oauth-input',
serviceId: '{service}',
requiredScopes: getScopesForService('{service}'),
required: true,
},
// Conditional fields per operation
@@ -409,7 +411,7 @@ If creating V2 versions (API-aligned outputs):
### Block
- [ ] Created `blocks/blocks/{service}.ts`
- [ ] Defined operation dropdown with all operations
- [ ] Added credential field (oauth-input or short-input)
- [ ] 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
@@ -419,6 +421,12 @@ If creating V2 versions (API-aligned outputs):
- [ ] 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`
@@ -717,6 +725,25 @@ Use `wandConfig` for fields that are hard to fill out manually:
}
```
### 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
@@ -729,3 +756,5 @@ Use `wandConfig` for fields that are hard to fill out manually:
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

@@ -26,8 +26,9 @@ 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 scopes (if OAuth service)
apps/sim/lib/oauth/oauth.ts # OAuth provider config (if OAuth service)
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
@@ -199,11 +200,14 @@ For **each tool** in `tools.access`:
## Step 5: Validate OAuth Scopes (if OAuth service)
- [ ] `auth.ts` scopes include ALL scopes needed by ALL tools in the integration
- [ ] `oauth.ts` provider config scopes match `auth.ts` scopes
- [ ] Block `requiredScopes` (if defined) matches `auth.ts` scopes
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
- [ ] Each scope has a human-readable description in `oauth-required-modal.tsx`'s `SCOPE_DESCRIPTIONS`
## Step 6: Validate Pagination Consistency
@@ -244,7 +248,8 @@ Group findings by severity:
- Missing `.trim()` on ID fields in request URLs
- Missing `?? null` on nullable response fields
- Block condition array missing an operation that uses that field
- Missing scope description in `oauth-required-modal.tsx`
- 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
@@ -273,7 +278,8 @@ After fixing, confirm:
- [ ] 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 alignment across auth.ts, oauth.ts, block, and modal (if OAuth)
- [ ] 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)

View File

@@ -0,0 +1,26 @@
---
description: SEO and GEO guidelines for the landing page
globs: ["apps/sim/app/(home)/**/*.tsx"]
---
# Landing Page — SEO / GEO
## SEO
- One `<h1>` per page, in Hero only — never add another.
- Strict heading hierarchy: H1 (Hero) → H2 (section titles) → H3 (feature names).
- Every section: `<section id="…" aria-labelledby="…-heading">`.
- Decorative/animated elements: `aria-hidden="true"`.
- All internal routes use Next.js `<Link>` (crawlable). External links get `rel="noopener noreferrer"`.
- Navbar is a Server Component (no `'use client'`) for immediate crawlability. Logo `<Image>` has `priority` (LCP element).
- Navbar `<nav>` carries `SiteNavigationElement` schema.org markup.
- Feature lists must stay in sync with `WebApplication.featureList` in `structured-data.tsx`.
## GEO (Generative Engine Optimisation)
- **Answer-first pattern**: each section's H2 + subtitle should directly answer a user question (e.g. "What is Sim?", "How fast can I deploy?").
- **Atomic answer blocks**: each feature / template card should be independently extractable by an AI summariser.
- **Entity consistency**: always write "Sim" by name — never "the platform" or "our tool".
- **Keyword density**: first 150 visible chars of Hero must name "Sim", "AI agents", "agentic workflows".
- **sr-only summaries**: Hero and Templates each have a `<p className="sr-only">` (~50 words) as an atomic product/catalog summary for AI citation.
- **Specific numbers**: prefer concrete figures ("1,000+ integrations", "15+ AI providers") over vague claims.

View File

@@ -5,62 +5,122 @@ globs: ["apps/sim/hooks/queries/**/*.ts"]
# React Query Patterns
All React Query hooks live in `hooks/queries/`.
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 query file defines a keys factory:
Every query file defines a hierarchical keys factory with an `all` root key and intermediate plural keys for prefix-level invalidation:
```typescript
export const entityKeys = {
all: ['entity'] as const,
list: (workspaceId?: string) => [...entityKeys.all, 'list', workspaceId ?? ''] as const,
detail: (id?: string) => [...entityKeys.all, 'detail', id ?? ''] 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,
}
```
Never use inline query keys — always use the factory.
## File Structure
```typescript
// 1. Query keys factory
// 2. Types (if needed)
// 3. Private fetch functions
// 3. Private fetch functions (accept signal parameter)
// 4. Exported hooks
```
## Query Hook
- Every `queryFn` must destructure and forward `signal` for request cancellation
- Every query must have an explicit `staleTime`
- Use `keepPreviousData` only on variable-key queries (where params change), never on static keys
```typescript
async function fetchEntities(workspaceId: string, signal?: AbortSignal) {
const response = await fetch(`/api/entities?workspaceId=${workspaceId}`, { signal })
if (!response.ok) throw new Error('Failed to fetch entities')
return response.json()
}
export function useEntityList(workspaceId?: string, options?: { enabled?: boolean }) {
return useQuery({
queryKey: entityKeys.list(workspaceId),
queryFn: () => fetchEntities(workspaceId as string),
queryFn: ({ signal }) => fetchEntities(workspaceId as string, signal),
enabled: Boolean(workspaceId) && (options?.enabled ?? true),
staleTime: 60 * 1000,
placeholderData: keepPreviousData,
placeholderData: keepPreviousData, // OK: workspaceId varies
})
}
```
## Mutation Hook
- Use targeted invalidation (`entityKeys.lists()`) not broad (`entityKeys.all`) when possible
- Invalidation must cover all affected query key prefixes (lists, details, related views)
```typescript
export function useCreateEntity() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (variables) => { /* fetch POST */ },
onSuccess: () => queryClient.invalidateQueries({ queryKey: entityKeys.all }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: entityKeys.lists() })
},
})
}
```
## Optimistic Updates
For optimistic mutations, use `onSettled` (not `onSuccess`) for cache reconciliation — `onSettled` fires on both success and error, ensuring the cache is always reconciled with the server.
```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 value */)
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) })
},
})
}
```
For optimistic mutations syncing with Zustand, use `createOptimisticMutationHandlers` from `@/hooks/queries/utils/optimistic-mutation`.
## useCallback Dependencies
Never include mutation objects (e.g., `createEntity`) in `useCallback` dependency arrays — the mutation object is not referentially stable and changes on every state update. The `.mutate()` and `.mutateAsync()` functions are stable in TanStack Query v5.
```typescript
// ✗ Bad — causes unnecessary recreations
const handler = useCallback(() => {
createEntity.mutate(data)
}, [createEntity]) // unstable reference
// ✓ Good — omit from deps, mutate is stable
const handler = useCallback(() => {
createEntity.mutate(data)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data])
```
## Naming
- **Keys**: `entityKeys`
- **Query hooks**: `useEntity`, `useEntityList`
- **Mutation hooks**: `useCreateEntity`, `useUpdateEntity`
- **Fetch functions**: `fetchEntity` (private)
- **Mutation hooks**: `useCreateEntity`, `useUpdateEntity`, `useDeleteEntity`
- **Fetch functions**: `fetchEntity`, `fetchEntities` (private)

View File

@@ -0,0 +1,257 @@
---
name: add-hosted-key
description: Add hosted API key support to a tool so Sim provides the key when users don't bring their own. Use when adding hosted keys, BYOK support, hideWhenHosted, or hosted key pricing to a tool or block.
---
# Adding Hosted Key Support to a Tool
When a tool has hosted key support, Sim provides its own API key if the user hasn't configured one (via BYOK or env var). Usage is metered and billed to the workspace.
## Overview
| Step | What | Where |
|------|------|-------|
| 1 | Register BYOK provider ID | `tools/types.ts`, `app/api/workspaces/[id]/byok-keys/route.ts` |
| 2 | Research the API's pricing and rate limits | API docs / pricing page (before writing any code) |
| 3 | Add `hosting` config to the tool | `tools/{service}/{action}.ts` |
| 4 | Hide API key field when hosted | `blocks/blocks/{service}.ts` |
| 5 | Add to BYOK settings UI | BYOK settings component (`byok.tsx`) |
| 6 | Summarize pricing and throttling comparison | Output to user (after all code changes) |
## Step 1: Register the BYOK Provider ID
Add the new provider to the `BYOKProviderId` union in `tools/types.ts`:
```typescript
export type BYOKProviderId =
| 'openai'
| 'anthropic'
// ...existing providers
| 'your_service'
```
Then add it to `VALID_PROVIDERS` in `app/api/workspaces/[id]/byok-keys/route.ts`:
```typescript
const VALID_PROVIDERS = ['openai', 'anthropic', 'google', 'mistral', 'your_service'] as const
```
## Step 2: Research the API's Pricing Model and Rate Limits
**Before writing any `getCost` or `rateLimit` code**, look up the service's official documentation for both pricing and rate limits. You need to understand:
### Pricing
1. **How the API charges** — per request, per credit, per token, per step, per minute, etc.
2. **Whether the API reports cost in its response** — look for fields like `creditsUsed`, `costDollars`, `tokensUsed`, or similar in the response body or headers
3. **Whether cost varies by endpoint/options** — some APIs charge more for certain features (e.g., Firecrawl charges 1 credit/page base but +4 for JSON format, +4 for enhanced mode)
4. **The dollar-per-unit rate** — what each credit/token/unit costs in dollars on our plan
### Rate Limits
1. **What rate limits the API enforces** — requests per minute/second, tokens per minute, concurrent requests, etc.
2. **Whether limits vary by plan tier** — free vs paid vs enterprise often have different ceilings
3. **Whether limits are per-key or per-account** — determines whether adding more hosted keys actually increases total throughput
4. **What the API returns when rate limited** — HTTP 429, `Retry-After` header, error body format, etc.
5. **Whether there are multiple dimensions** — some APIs limit both requests/min AND tokens/min independently
Search the API's docs/pricing page (use WebSearch/WebFetch). Capture the pricing model as a comment in `getCost` so future maintainers know the source of truth.
### Setting Our Rate Limits
Our rate limiter (`lib/core/rate-limiter/hosted-key/`) uses a token-bucket algorithm applied **per billing actor** (workspace). It supports two modes:
- **`per_request`** — simple; just `requestsPerMinute`. Good when the API charges flat per-request or cost doesn't vary much.
- **`custom`** — `requestsPerMinute` plus additional `dimensions` (e.g., `tokens`, `search_units`). Each dimension has its own `limitPerMinute` and an `extractUsage` function that reads actual usage from the response. Use when the API charges on a variable metric (tokens, credits) and you want to cap that metric too.
When choosing values for `requestsPerMinute` and any dimension limits:
- **Stay well below the API's per-key limit** — our keys are shared across all workspaces. If the API allows 60 RPM per key and we have 3 keys, the global ceiling is ~180 RPM. Set the per-workspace limit low enough (e.g., 20-60 RPM) that many workspaces can coexist without collectively hitting the API's ceiling.
- **Account for key pooling** — our round-robin distributes requests across `N` hosted keys, so the effective API-side rate per key is `(total requests) / N`. But per-workspace limits are enforced *before* key selection, so they apply regardless of key count.
- **Prefer conservative defaults** — it's easy to raise limits later but hard to claw back after users depend on high throughput.
## Step 3: Add `hosting` Config to the Tool
Add a `hosting` object to the tool's `ToolConfig`. This tells the execution layer how to acquire hosted keys, calculate cost, and rate-limit.
```typescript
hosting: {
envKeyPrefix: 'YOUR_SERVICE_API_KEY',
apiKeyParam: 'apiKey',
byokProviderId: 'your_service',
pricing: {
type: 'custom',
getCost: (_params, output) => {
if (output.creditsUsed == null) {
throw new Error('Response missing creditsUsed field')
}
const creditsUsed = output.creditsUsed as number
const cost = creditsUsed * 0.001 // dollars per credit
return { cost, metadata: { creditsUsed } }
},
},
rateLimit: {
mode: 'per_request',
requestsPerMinute: 100,
},
},
```
### Hosted Key Env Var Convention
Keys use a numbered naming pattern driven by a count env var:
```
YOUR_SERVICE_API_KEY_COUNT=3
YOUR_SERVICE_API_KEY_1=sk-...
YOUR_SERVICE_API_KEY_2=sk-...
YOUR_SERVICE_API_KEY_3=sk-...
```
The `envKeyPrefix` value (`YOUR_SERVICE_API_KEY`) determines which env vars are read at runtime. Adding more keys only requires bumping the count and adding the new env var.
### Pricing: Prefer API-Reported Cost
Always prefer using cost data returned by the API (e.g., `creditsUsed`, `costDollars`). This is the most accurate because it accounts for variable pricing tiers, feature modifiers, and plan-level discounts.
**When the API reports cost** — use it directly and throw if missing:
```typescript
pricing: {
type: 'custom',
getCost: (params, output) => {
if (output.creditsUsed == null) {
throw new Error('Response missing creditsUsed field')
}
// $0.001 per credit — from https://example.com/pricing
const cost = (output.creditsUsed as number) * 0.001
return { cost, metadata: { creditsUsed: output.creditsUsed } }
},
},
```
**When the API does NOT report cost** — compute it from params/output based on the pricing docs, but still validate the data you depend on:
```typescript
pricing: {
type: 'custom',
getCost: (params, output) => {
if (!Array.isArray(output.searchResults)) {
throw new Error('Response missing searchResults, cannot determine cost')
}
// Serper: 1 credit for <=10 results, 2 credits for >10 — from https://serper.dev/pricing
const credits = Number(params.num) > 10 ? 2 : 1
return { cost: credits * 0.001, metadata: { credits } }
},
},
```
**`getCost` must always throw** if it cannot determine cost. Never silently fall back to a default — this would hide billing inaccuracies.
### Capturing Cost Data from the API
If the API returns cost info, capture it in `transformResponse` so `getCost` can read it from the output:
```typescript
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
results: data.results,
creditsUsed: data.creditsUsed, // pass through for getCost
},
}
},
```
For async/polling tools, capture it in `postProcess` when the job completes:
```typescript
if (jobData.status === 'completed') {
result.output = {
data: jobData.data,
creditsUsed: jobData.creditsUsed,
}
}
```
## Step 4: Hide the API Key Field When Hosted
In the block config (`blocks/blocks/{service}.ts`), add `hideWhenHosted: true` to the API key subblock. This hides the field on hosted Sim since the platform provides the key:
```typescript
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your API key',
password: true,
required: true,
hideWhenHosted: true,
},
```
The visibility is controlled by `isSubBlockHiddenByHostedKey()` in `lib/workflows/subblocks/visibility.ts`, which checks the `isHosted` feature flag.
## Step 5: Add to the BYOK Settings UI
Add an entry to the `PROVIDERS` array in the BYOK settings component so users can bring their own key. You need the service icon from `components/icons.tsx`:
```typescript
{
id: 'your_service',
name: 'Your Service',
icon: YourServiceIcon,
description: 'What this service does',
placeholder: 'Enter your API key',
},
```
## Step 6: Summarize Pricing and Throttling Comparison
After all code changes are complete, output a detailed summary to the user covering:
### What to include
1. **API's pricing model** — how the service charges (per token, per credit, per request, etc.), the specific rates found in docs, and whether the API reports cost in responses.
2. **Our `getCost` approach** — how we calculate cost, what fields we depend on, and any assumptions or estimates (especially when the API doesn't report exact dollar cost).
3. **API's rate limits** — the documented limits (RPM, TPM, concurrent, etc.), which plan tier they apply to, and whether they're per-key or per-account.
4. **Our `rateLimit` config** — what we set for `requestsPerMinute` (and dimensions if custom mode), why we chose those values, and how they compare to the API's limits.
5. **Key pooling impact** — how many hosted keys we expect, and how round-robin distribution affects the effective per-key rate at the API.
6. **Gaps or risks** — anything the API charges for that we don't meter, rate limit dimensions we chose not to enforce, or pricing that may be inaccurate due to variable model/tier costs.
### Format
Present this as a structured summary with clear headings. Example:
```
### Pricing
- **API charges**: $X per 1M tokens (input), $Y per 1M tokens (output) — varies by model
- **Response reports cost?**: No — only token counts in `usage` field
- **Our getCost**: Estimates cost at $Z per 1M total tokens based on median model pricing
- **Risk**: Actual cost varies by model; our estimate may over/undercharge for cheap/expensive models
### Throttling
- **API limits**: 300 RPM per key (paid tier), 60 RPM (free tier)
- **Per-key or per-account**: Per key — more keys = more throughput
- **Our config**: 60 RPM per workspace (per_request mode)
- **With N keys**: Effective per-key rate is (total RPM across workspaces) / N
- **Headroom**: Comfortable — even 10 active workspaces at full rate = 600 RPM / 3 keys = 200 RPM per key, under the 300 RPM API limit
```
This summary helps reviewers verify that the pricing and rate limiting are well-calibrated and surfaces any risks that need monitoring.
## Checklist
- [ ] Provider added to `BYOKProviderId` in `tools/types.ts`
- [ ] Provider added to `VALID_PROVIDERS` in the BYOK keys API route
- [ ] API pricing docs researched — understand per-unit cost and whether the API reports cost in responses
- [ ] API rate limits researched — understand RPM/TPM limits, per-key vs per-account, and plan tiers
- [ ] `hosting` config added to the tool with `envKeyPrefix`, `apiKeyParam`, `byokProviderId`, `pricing`, and `rateLimit`
- [ ] `getCost` throws if required cost data is missing from the response
- [ ] Cost data captured in `transformResponse` or `postProcess` if API provides it
- [ ] `hideWhenHosted: true` added to the API key subblock in the block config
- [ ] Provider entry added to the BYOK settings UI with icon and description
- [ ] Env vars documented: `{PREFIX}_COUNT` and `{PREFIX}_1..N`
- [ ] Pricing and throttling summary provided to reviewer

View File

@@ -1,4 +1,4 @@
FROM oven/bun:1.3.9-alpine
FROM oven/bun:1.3.10-alpine
# Install necessary packages for development
RUN apk add --no-cache \

View File

@@ -20,7 +20,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.9
bun-version: 1.3.10
- 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.9
bun-version: 1.3.10
- 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.9
bun-version: 1.3.10
- 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.9
bun-version: 1.3.10
- 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.9
bun-version: 1.3.10
- 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.9
bun-version: 1.3.10
- 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.9
bun-version: 1.3.10
- name: Setup Node
uses: actions/setup-node@v4
@@ -90,6 +90,16 @@ jobs:
echo "✅ All feature flags are properly configured"
- name: Check subblock ID stability
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE_REF="origin/${{ github.base_ref }}"
git fetch --depth=1 origin "${{ github.base_ref }}" 2>/dev/null || true
else
BASE_REF="HEAD~1"
fi
bun run apps/sim/scripts/check-subblock-id-stability.ts "$BASE_REF"
- name: Lint code
run: bun run lint:check

3
.gitignore vendored
View File

@@ -26,6 +26,9 @@ bun-debug.log*
**/standalone/
sim-standalone.tar.gz
# redis
dump.rdb
# misc
.DS_Store
*.pem

View File

@@ -134,21 +134,64 @@ Use `devtools` middleware. Use `persist` only when data should survive reload wi
## React Query
All React Query hooks live in `hooks/queries/`.
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,
list: (workspaceId?: string) => [...entityKeys.all, 'list', workspaceId ?? ''] 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: () => fetchEntities(workspaceId as string),
queryFn: ({ signal }) => fetchEntities(workspaceId as string, signal),
enabled: Boolean(workspaceId),
staleTime: 60 * 1000,
placeholderData: keepPreviousData,
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) })
},
})
}
```

View File

@@ -4,7 +4,7 @@
</a>
</p>
<p align="center">Build and deploy AI agent workflows in minutes.</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>

View File

@@ -233,6 +233,7 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
lang={lang}
breadcrumb={breadcrumbs}
/>
<style>{`#nd-page { grid-column: main-start / toc-end !important; max-width: 1400px !important; }`}</style>
<DocsPage
toc={data.toc}
breadcrumb={{
@@ -367,15 +368,17 @@ export async function generateMetadata(props: {
return {
title: data.title,
description:
data.description || 'Sim visual workflow builder for AI applications documentation',
data.description ||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce.',
keywords: [
'AI workflow builder',
'visual workflow editor',
'AI automation',
'workflow automation',
'AI agents',
'no-code AI',
'drag and drop workflows',
'agentic workforce',
'AI agent platform',
'agentic workflows',
'LLM orchestration',
'AI automation',
'knowledge base',
'AI integrations',
data.title?.toLowerCase().split(' '),
]
.flat()
@@ -385,7 +388,8 @@ export async function generateMetadata(props: {
openGraph: {
title: data.title,
description:
data.description || 'Sim visual workflow builder for AI applications documentation',
data.description ||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce.',
url: fullUrl,
siteName: 'Sim Documentation',
type: 'article',
@@ -406,7 +410,8 @@ export async function generateMetadata(props: {
card: 'summary_large_image',
title: data.title,
description:
data.description || 'Sim visual workflow builder for AI applications documentation',
data.description ||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce.',
images: [ogImageUrl],
creator: '@simdotai',
site: '@simdotai',

View File

@@ -66,7 +66,7 @@ export default async function Layout({ children, params }: LayoutProps) {
'@type': 'WebSite',
name: 'Sim Documentation',
description:
'Comprehensive documentation for Sim - the visual workflow builder for AI Agent Workflows.',
'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.',
url: 'https://docs.sim.ai',
publisher: {
'@type': 'Organization',

View File

@@ -552,16 +552,15 @@ video {
/* API Reference Pages — Mintlify-style overrides */
/* OpenAPI pages: span main + TOC grid columns for wide two-column layout.
The grid has columns: spacer | sidebar | main | toc | spacer.
By spanning columns 3-4, the article fills both main and toc areas,
while the grid structure stays identical to non-OpenAPI pages (no jitter). */
Use named grid lines from grid-template-areas so this works regardless
of whether the grid has 3 columns (production) or 5 columns (local dev). */
#nd-page:has(.api-page-header) {
grid-column: 3 / span 2 !important;
grid-column: main-start / toc-end !important;
max-width: 1400px !important;
}
/* Hide the empty TOC aside on OpenAPI pages so it doesn't overlay content */
#nd-docs-layout:has(#nd-page .api-page-header) #nd-toc {
#nd-docs-layout:has(#nd-page:has(.api-page-header)) #nd-toc {
display: none;
}
@@ -590,44 +589,39 @@ video {
"Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
/* Method badge pills in page content — colored background pills */
#nd-page span.font-mono.font-medium[class*="text-green"] {
background-color: rgb(220 252 231 / 0.6);
padding: 0.125rem 0.5rem;
border-radius: 0.375rem;
font-size: 0.75rem;
/* Method badge pills — shared background colors (page + sidebar) */
span.font-mono.font-medium[data-method="get"],
span.font-mono.font-medium[data-method="head"],
span.font-mono.font-medium[data-method="options"] {
background-color: rgb(220 252 231 / 0.85);
}
html.dark #nd-page span.font-mono.font-medium[class*="text-green"] {
html.dark span.font-mono.font-medium[data-method="get"],
html.dark span.font-mono.font-medium[data-method="head"],
html.dark span.font-mono.font-medium[data-method="options"] {
background-color: rgb(34 197 94 / 0.15);
}
#nd-page span.font-mono.font-medium[class*="text-blue"] {
background-color: rgb(219 234 254 / 0.6);
padding: 0.125rem 0.5rem;
border-radius: 0.375rem;
font-size: 0.75rem;
span.font-mono.font-medium[data-method="post"] {
background-color: rgb(219 234 254 / 0.85);
}
html.dark #nd-page span.font-mono.font-medium[class*="text-blue"] {
html.dark span.font-mono.font-medium[data-method="post"] {
background-color: rgb(59 130 246 / 0.15);
}
#nd-page span.font-mono.font-medium[class*="text-orange"] {
background-color: rgb(255 237 213 / 0.6);
padding: 0.125rem 0.5rem;
border-radius: 0.375rem;
font-size: 0.75rem;
span.font-mono.font-medium[data-method="put"] {
background-color: rgb(254 249 195 / 0.85);
}
html.dark #nd-page span.font-mono.font-medium[class*="text-orange"] {
html.dark span.font-mono.font-medium[data-method="put"] {
background-color: rgb(234 179 8 / 0.15);
}
span.font-mono.font-medium[data-method="patch"] {
background-color: rgb(255 237 213 / 0.85);
}
html.dark span.font-mono.font-medium[data-method="patch"] {
background-color: rgb(249 115 22 / 0.15);
}
#nd-page span.font-mono.font-medium[class*="text-red"] {
background-color: rgb(254 226 226 / 0.6);
padding: 0.125rem 0.5rem;
border-radius: 0.375rem;
font-size: 0.75rem;
span.font-mono.font-medium[data-method="delete"] {
background-color: rgb(254 226 226 / 0.85);
}
html.dark #nd-page span.font-mono.font-medium[class*="text-red"] {
html.dark span.font-mono.font-medium[data-method="delete"] {
background-color: rgb(239 68 68 / 0.15);
}
@@ -635,52 +629,31 @@ html.dark #nd-page span.font-mono.font-medium[class*="text-red"] {
#nd-sidebar a:has(span.font-mono.font-medium) {
display: flex !important;
align-items: center !important;
gap: 6px;
gap: 0.375rem;
}
/* Sidebar method badges — ensure proper inline flex display */
/* Sidebar method badges — fixed-width for right-aligned labels */
#nd-sidebar a span.font-mono.font-medium {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2.25rem;
font-size: 10px !important;
width: 2.625rem;
font-size: 0.625rem !important;
line-height: 1 !important;
padding: 2.5px 4px;
border-radius: 3px;
padding: 0.15625rem 0.25rem;
border-radius: 0.1875rem;
flex-shrink: 0;
}
/* Sidebar GET badges */
#nd-sidebar a span.font-mono.font-medium[class*="text-green"] {
background-color: rgb(220 252 231 / 0.6);
}
html.dark #nd-sidebar a span.font-mono.font-medium[class*="text-green"] {
background-color: rgb(34 197 94 / 0.15);
}
/* Sidebar POST badges */
#nd-sidebar a span.font-mono.font-medium[class*="text-blue"] {
background-color: rgb(219 234 254 / 0.6);
}
html.dark #nd-sidebar a span.font-mono.font-medium[class*="text-blue"] {
background-color: rgb(59 130 246 / 0.15);
}
/* Sidebar PUT badges */
#nd-sidebar a span.font-mono.font-medium[class*="text-orange"] {
background-color: rgb(255 237 213 / 0.6);
}
html.dark #nd-sidebar a span.font-mono.font-medium[class*="text-orange"] {
background-color: rgb(249 115 22 / 0.15);
}
/* Sidebar DELETE badges */
#nd-sidebar a span.font-mono.font-medium[class*="text-red"] {
background-color: rgb(254 226 226 / 0.6);
}
html.dark #nd-sidebar a span.font-mono.font-medium[class*="text-red"] {
background-color: rgb(239 68 68 / 0.15);
/* Footer navigation method badges — pill styling to match sidebar */
#nd-page span.font-mono.font-medium[data-method] {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.625rem !important;
line-height: 1 !important;
padding: 0.15625rem 0.375rem;
border-radius: 0.1875rem;
}
/* Code block containers — match regular docs styling */
@@ -740,8 +713,25 @@ html.dark
font-size: 0.6875rem !important;
letter-spacing: 0.025em;
text-transform: uppercase;
padding: 0.125rem 0.5rem !important;
border-radius: 0.375rem !important;
}
/* POST — softer blue */
/* Path bar per-method colors (fumadocs renders these, so we match by class) */
/* GET */
#nd-page:has(.api-page-header)
div.flex.flex-row.items-center.rounded-xl.border.not-prose
span.font-mono.font-medium[class*="text-green"] {
color: rgb(22 163 74) !important;
background-color: rgb(220 252 231 / 0.7) !important;
}
html.dark
#nd-page:has(.api-page-header)
div.flex.flex-row.items-center.rounded-xl.border.not-prose
span.font-mono.font-medium[class*="text-green"] {
color: rgb(74 222 128) !important;
background-color: rgb(34 197 94 / 0.15) !important;
}
/* POST */
#nd-page:has(.api-page-header)
div.flex.flex-row.items-center.rounded-xl.border.not-prose
span.font-mono.font-medium[class*="text-blue"] {
@@ -755,19 +745,47 @@ html.dark
color: rgb(96 165 250) !important;
background-color: rgb(59 130 246 / 0.15) !important;
}
/* GET — softer green */
/* PUT */
#nd-page:has(.api-page-header)
div.flex.flex-row.items-center.rounded-xl.border.not-prose
span.font-mono.font-medium[class*="text-green"] {
color: rgb(22 163 74) !important;
background-color: rgb(220 252 231 / 0.7) !important;
span.font-mono.font-medium[class*="text-yellow"] {
color: rgb(161 98 7) !important;
background-color: rgb(254 249 195 / 0.7) !important;
}
html.dark
#nd-page:has(.api-page-header)
div.flex.flex-row.items-center.rounded-xl.border.not-prose
span.font-mono.font-medium[class*="text-green"] {
color: rgb(74 222 128) !important;
background-color: rgb(34 197 94 / 0.15) !important;
span.font-mono.font-medium[class*="text-yellow"] {
color: rgb(250 204 21) !important;
background-color: rgb(234 179 8 / 0.15) !important;
}
/* PATCH */
#nd-page:has(.api-page-header)
div.flex.flex-row.items-center.rounded-xl.border.not-prose
span.font-mono.font-medium[class*="text-orange"] {
color: rgb(194 65 12) !important;
background-color: rgb(255 237 213 / 0.7) !important;
}
html.dark
#nd-page:has(.api-page-header)
div.flex.flex-row.items-center.rounded-xl.border.not-prose
span.font-mono.font-medium[class*="text-orange"] {
color: rgb(251 146 60) !important;
background-color: rgb(249 115 22 / 0.15) !important;
}
/* DELETE */
#nd-page:has(.api-page-header)
div.flex.flex-row.items-center.rounded-xl.border.not-prose
span.font-mono.font-medium[class*="text-red"] {
color: rgb(185 28 28) !important;
background-color: rgb(254 226 226 / 0.7) !important;
}
html.dark
#nd-page:has(.api-page-header)
div.flex.flex-row.items-center.rounded-xl.border.not-prose
span.font-mono.font-medium[class*="text-red"] {
color: rgb(248 113 113) !important;
background-color: rgb(239 68 68 / 0.15) !important;
}
/* Path text inside method+path bar — monospace, bright like Gumloop */
@@ -966,17 +984,17 @@ html.dark .response-section-dropdown-item:hover {
order: 1;
}
/* Type badge — order 2, grey pill like Mintlify */
/* Type badge — order 2, grey pill */
#nd-page:has(.api-page-header)
.flex.flex-wrap.items-center.gap-3.not-prose
> span.text-sm.font-mono.text-fd-muted-foreground {
order: 2;
background-color: rgb(240 240 243);
color: rgb(100 100 110);
padding: 0.125rem 0.5rem;
background-color: rgb(241 245 249);
color: rgb(71 85 105);
padding: 0.1875rem 0.5rem;
border-radius: 0.375rem;
font-size: 0.6875rem;
line-height: 1.25rem;
line-height: 1.125rem;
font-weight: 500;
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
}
@@ -984,8 +1002,8 @@ html.dark
#nd-page:has(.api-page-header)
.flex.flex-wrap.items-center.gap-3.not-prose
> span.text-sm.font-mono.text-fd-muted-foreground {
background-color: rgb(39 39 42);
color: rgb(212 212 216);
background-color: rgb(51 51 56);
color: rgb(212 212 220);
}
/* Hide the "*" inside the name span — we'll add "required" as a ::after on the flex row */
@@ -993,26 +1011,26 @@ html.dark
display: none;
}
/* Required badge — order 3, light red pill */
/* Required badge — order 3, red pill */
#nd-page:has(.api-page-header)
.flex.flex-wrap.items-center.gap-3.not-prose:has(span.text-red-400)::after {
content: "required";
order: 3;
display: inline-flex;
align-items: center;
background-color: rgb(254 235 235);
color: rgb(220 38 38);
padding: 0.125rem 0.5rem;
background-color: rgb(254 226 226);
color: rgb(185 28 28);
padding: 0.1875rem 0.5rem;
border-radius: 0.375rem;
font-size: 0.6875rem;
line-height: 1.25rem;
line-height: 1.125rem;
font-weight: 500;
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
}
html.dark
#nd-page:has(.api-page-header)
.flex.flex-wrap.items-center.gap-3.not-prose:has(span.text-red-400)::after {
background-color: rgb(127 29 29 / 0.2);
background-color: rgb(153 27 27 / 0.3);
color: rgb(252 165 165);
}
@@ -1054,12 +1072,12 @@ html.dark
> span.text-sm.font-mono.text-fd-muted-foreground::after {
content: "string";
font-size: 0.6875rem;
line-height: 1.25rem;
line-height: 1.125rem;
font-weight: 500;
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
background-color: rgb(240 240 243);
color: rgb(100 100 110);
padding: 0.125rem 0.5rem;
background-color: rgb(241 245 249);
color: rgb(71 85 105);
padding: 0.1875rem 0.5rem;
border-radius: 0.375rem;
display: inline-flex;
align-items: center;
@@ -1069,8 +1087,8 @@ html.dark
div.my-4
> .flex.flex-wrap.items-center.gap-3.not-prose
> span.text-sm.font-mono.text-fd-muted-foreground::after {
background-color: rgb(39 39 42);
color: rgb(212 212 216);
background-color: rgb(51 51 56);
color: rgb(212 212 220);
}
/* "header" badge via ::before on the auth flex row */
@@ -1079,12 +1097,12 @@ html.dark
order: 3;
display: inline-flex;
align-items: center;
background-color: rgb(240 240 243);
color: rgb(100 100 110);
padding: 0.125rem 0.5rem;
background-color: rgb(241 245 249);
color: rgb(71 85 105);
padding: 0.1875rem 0.5rem;
border-radius: 0.375rem;
font-size: 0.6875rem;
line-height: 1.25rem;
line-height: 1.125rem;
font-weight: 500;
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
}
@@ -1092,22 +1110,22 @@ html.dark
#nd-page:has(.api-page-header)
div.my-4
> .flex.flex-wrap.items-center.gap-3.not-prose::before {
background-color: rgb(39 39 42);
color: rgb(212 212 216);
background-color: rgb(51 51 56);
color: rgb(212 212 220);
}
/* "required" badge via ::after on the auth flex row — light red pill */
/* "required" badge via ::after on the auth flex row — red pill */
#nd-page:has(.api-page-header) div.my-4 > .flex.flex-wrap.items-center.gap-3.not-prose::after {
content: "required";
order: 4;
display: inline-flex;
align-items: center;
background-color: rgb(254 235 235);
color: rgb(220 38 38);
padding: 0.125rem 0.5rem;
background-color: rgb(254 226 226);
color: rgb(185 28 28);
padding: 0.1875rem 0.5rem;
border-radius: 0.375rem;
font-size: 0.6875rem;
line-height: 1.25rem;
line-height: 1.125rem;
font-weight: 500;
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
}
@@ -1115,7 +1133,7 @@ html.dark
#nd-page:has(.api-page-header)
div.my-4
> .flex.flex-wrap.items-center.gap-3.not-prose::after {
background-color: rgb(127 29 29 / 0.2);
background-color: rgb(153 27 27 / 0.3);
color: rgb(252 165 165);
}
@@ -1168,12 +1186,12 @@ html.dark #nd-page:has(.api-page-header) .text-sm.border-t {
#nd-page:has(.api-page-header) .flex.flex-wrap.items-center.gap-3.not-prose > button,
#nd-page:has(.api-page-header) .flex.flex-wrap.items-center.gap-3.not-prose > span:has(> button) {
order: 2;
background-color: rgb(240 240 243);
color: rgb(100 100 110);
padding: 0.125rem 0.5rem;
background-color: rgb(241 245 249);
color: rgb(71 85 105);
padding: 0.1875rem 0.5rem;
border-radius: 0.375rem;
font-size: 0.6875rem;
line-height: 1.25rem;
line-height: 1.125rem;
font-weight: 500;
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
}
@@ -1182,8 +1200,8 @@ html.dark
#nd-page:has(.api-page-header)
.flex.flex-wrap.items-center.gap-3.not-prose
> span:has(> button) {
background-color: rgb(39 39 42);
color: rgb(212 212 216);
background-color: rgb(51 51 56);
color: rgb(212 212 220);
}
/* Section headings (Authorization, Path Parameters, etc.) — consistent top spacing */

View File

@@ -7,26 +7,27 @@ export default function RootLayout({ children }: { children: ReactNode }) {
export const metadata = {
metadataBase: new URL('https://docs.sim.ai'),
title: {
default: 'Sim Documentation - Visual Workflow Builder for AI Applications',
default: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce',
template: '%s',
},
description:
'Comprehensive documentation for Sim - the visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines by connecting blocks on a canvas—no coding required.',
'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.',
keywords: [
'AI workflow builder',
'visual workflow editor',
'AI automation',
'workflow automation',
'AI agents',
'no-code AI',
'drag and drop workflows',
'agentic workforce',
'AI agent platform',
'open-source AI agents',
'agentic workflows',
'LLM orchestration',
'AI integrations',
'workflow canvas',
'AI Agent Workflow Builder',
'workflow orchestration',
'agent builder',
'AI workflow automation',
'visual programming',
'knowledge base',
'AI automation',
'workflow builder',
'AI workflow orchestration',
'enterprise AI',
'AI agent deployment',
'intelligent automation',
'AI tools',
],
authors: [{ name: 'Sim Team', url: 'https://sim.ai' }],
creator: 'Sim',
@@ -53,9 +54,9 @@ export const metadata = {
alternateLocale: ['es_ES', 'fr_FR', 'de_DE', 'ja_JP', 'zh_CN'],
url: 'https://docs.sim.ai',
siteName: 'Sim Documentation',
title: 'Sim Documentation - Visual Workflow Builder for AI Applications',
title: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce',
description:
'Comprehensive documentation for Sim - the visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines.',
'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.',
images: [
{
url: 'https://docs.sim.ai/api/og?title=Sim%20Documentation',
@@ -67,9 +68,9 @@ export const metadata = {
},
twitter: {
card: 'summary_large_image',
title: 'Sim Documentation - Visual Workflow Builder for AI Applications',
title: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce',
description:
'Comprehensive documentation for Sim - the visual workflow builder for AI applications.',
'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.',
creator: '@simdotai',
site: '@simdotai',
images: ['https://docs.sim.ai/api/og?title=Sim%20Documentation'],

View File

@@ -37,9 +37,9 @@ export async function GET() {
const manifest = `# Sim Documentation
> Visual Workflow Builder for AI Applications
> The open-source platform to build AI agents and run your agentic workforce.
Sim is a visual workflow builder for AI applications that lets you build AI agent workflows visually. Create powerful AI agents, automation workflows, and data processing pipelines by connecting blocks on a canvas—no coding required.
Sim is 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. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders.
## Documentation Overview

View File

@@ -10,9 +10,9 @@ export function TOCFooter() {
<div className='text-balance font-semibold text-base leading-tight'>
Start building today
</div>
<div className='text-muted-foreground'>Trusted by over 70,000 builders.</div>
<div className='text-muted-foreground'>Trusted by over 100,000 builders.</div>
<div className='text-muted-foreground'>
Build Agentic workflows visually on a drag-and-drop canvas or with natural language.
The open-source platform to build AI agents and run your agentic workforce.
</div>
<Link
href='https://sim.ai/signup'

View File

@@ -548,6 +548,34 @@ export function GithubIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function GithubOutlineIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M15 21C15 21 15 18.73 15 18C15 17.37 15.15 16.04 14.5 15.5C15.89 15.37 16.98 14.92 18 14C19.02 13.08 19.5 11.69 19.5 9.5C19.5 8 19.25 7 18.5 6C18.79 5.22 18.84 4 18.5 3C16.94 3 15.53 4.07 15 4.5C14.61 4.4 13.67 4 12 4C10.33 4 9.39 4.4 9 4.5C8.47 4.07 7.06 3 5.5 3C5.16 4 5.21 5.22 5.5 6C4.75 7 4.5 8 4.5 9.5C4.5 11.69 4.98 13.08 6 14C7.02 14.92 8.11 15.37 9.5 15.5C8.85 16.04 9 17.37 9 18C9 18.73 9 21 9 21'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
/>
<path
d='M9 19C7.59 19 6.16 18.44 5.31 17.81C4.47 17.18 4.22 16.15 3 15.5'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
)
}
export function GitLabIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} width='24' height='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
@@ -710,6 +738,155 @@ export function PerplexityIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function ObsidianIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
const bl = `${id}-bl`
const tr = `${id}-tr`
const tl = `${id}-tl`
const br = `${id}-br`
const te = `${id}-te`
const le = `${id}-le`
const be = `${id}-be`
const me = `${id}-me`
const clip = `${id}-clip`
return (
<svg {...props} viewBox='0 0 512 512' fill='none' xmlns='http://www.w3.org/2000/svg'>
<radialGradient
id={bl}
cx='0'
cy='0'
gradientTransform='matrix(-59 -225 150 -39 161.4 470)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.4' />
<stop offset='1' stopOpacity='.1' />
</radialGradient>
<radialGradient
id={tr}
cx='0'
cy='0'
gradientTransform='matrix(50 -379 280 37 360 374.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.6' />
<stop offset='1' stopColor='#fff' stopOpacity='.1' />
</radialGradient>
<radialGradient
id={tl}
cx='0'
cy='0'
gradientTransform='matrix(69 -319 218 47 175.4 307)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.8' />
<stop offset='1' stopColor='#fff' stopOpacity='.4' />
</radialGradient>
<radialGradient
id={br}
cx='0'
cy='0'
gradientTransform='matrix(-96 -163 187 -111 335.3 512.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.3' />
<stop offset='1' stopOpacity='.3' />
</radialGradient>
<radialGradient
id={te}
cx='0'
cy='0'
gradientTransform='matrix(-36 166 -112 -24 310 128.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='0' />
<stop offset='1' stopColor='#fff' stopOpacity='.2' />
</radialGradient>
<radialGradient
id={le}
cx='0'
cy='0'
gradientTransform='matrix(88 89 -190 187 111 220.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.2' />
<stop offset='1' stopColor='#fff' stopOpacity='.4' />
</radialGradient>
<radialGradient
id={be}
cx='0'
cy='0'
gradientTransform='matrix(9 130 -276 20 215 284)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.2' />
<stop offset='1' stopColor='#fff' stopOpacity='.3' />
</radialGradient>
<radialGradient
id={me}
cx='0'
cy='0'
gradientTransform='matrix(-198 -104 327 -623 400 399.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.2' />
<stop offset='.5' stopColor='#fff' stopOpacity='.2' />
<stop offset='1' stopColor='#fff' stopOpacity='.3' />
</radialGradient>
<clipPath id={clip}>
<path d='M.2.2h512v512H.2z' />
</clipPath>
<g clipPath={`url(#${clip})`}>
<path
d='M382.3 475.6c-3.1 23.4-26 41.6-48.7 35.3-32.4-8.9-69.9-22.8-103.6-25.4l-51.7-4a34 34 0 0 1-22-10.2l-89-91.7a34 34 0 0 1-6.7-37.7s55-121 57.1-127.3c2-6.3 9.6-61.2 14-90.6 1.2-7.9 5-15 11-20.3L248 8.9a34.1 34.1 0 0 1 49.6 4.3L386 125.6a37 37 0 0 1 7.6 22.4c0 21.3 1.8 65 13.6 93.2 11.5 27.3 32.5 57 43.5 71.5a17.3 17.3 0 0 1 1.3 19.2 1494 1494 0 0 1-44.8 70.6c-15 22.3-21.9 49.9-25 73.1z'
fill='#6c31e3'
/>
<path
d='M165.9 478.3c41.4-84 40.2-144.2 22.6-187-16.2-39.6-46.3-64.5-70-80-.6 2.3-1.3 4.4-2.2 6.5L60.6 342a34 34 0 0 0 6.6 37.7l89.1 91.7a34 34 0 0 0 9.6 7z'
fill={`url(#${bl})`}
/>
<path
d='M278.4 307.8c11.2 1.2 22.2 3.6 32.8 7.6 34 12.7 65 41.2 90.5 96.3 1.8-3.1 3.6-6.2 5.6-9.2a1536 1536 0 0 0 44.8-70.6 17 17 0 0 0-1.3-19.2c-11-14.6-32-44.2-43.5-71.5-11.8-28.2-13.5-72-13.6-93.2 0-8.1-2.6-16-7.6-22.4L297.6 13.2a34 34 0 0 0-1.5-1.7 96 96 0 0 1 2 54 198.3 198.3 0 0 1-17.6 41.3l-7.2 14.2a171 171 0 0 0-19.4 71c-1.2 29.4 4.8 66.4 24.5 115.8z'
fill={`url(#${tr})`}
/>
<path
d='M278.4 307.8c-19.7-49.4-25.8-86.4-24.5-115.9a171 171 0 0 1 19.4-71c2.3-4.8 4.8-9.5 7.2-14.1 7.1-13.9 14-27 17.6-41.4a96 96 0 0 0-2-54A34.1 34.1 0 0 0 248 9l-105.4 94.8a34.1 34.1 0 0 0-10.9 20.3l-12.8 85-.5 2.3c23.8 15.5 54 40.4 70.1 80a147 147 0 0 1 7.8 24.8c28-6.8 55.7-11 82.1-8.3z'
fill={`url(#${tl})`}
/>
<path
d='M333.6 511c22.7 6.2 45.6-12 48.7-35.4a187 187 0 0 1 19.4-63.9c-25.6-55-56.5-83.6-90.4-96.3-36-13.4-75.2-9-115 .7 8.9 40.4 3.6 93.3-30.4 162.2 4 1.8 8.1 3 12.5 3.3 0 0 24.4 2 53.6 4.1 29 2 72.4 17.1 101.6 25.2z'
fill={`url(#${br})`}
/>
<g clipRule='evenodd' fillRule='evenodd'>
<path
d='M254.1 190c-1.3 29.2 2.4 62.8 22.1 112.1l-6.2-.5c-17.7-51.5-21.5-78-20.2-107.6a174.7 174.7 0 0 1 20.4-72c2.4-4.9 8-14.1 10.5-18.8 7.1-13.7 11.9-21 16-33.6 5.7-17.5 4.5-25.9 3.8-34.1 4.6 29.9-12.7 56-25.7 82.4a177.1 177.1 0 0 0-20.7 72z'
fill={`url(#${te})`}
/>
<path
d='M194.3 293.4c2.4 5.4 4.6 9.8 6 16.5L195 311c-2.1-7.8-3.8-13.4-6.8-20-17.8-42-46.3-63.6-69.7-79.5 28.2 15.2 57.2 39 75.7 81.9z'
fill={`url(#${le})`}
/>
<path
d='M200.6 315.1c9.8 46-1.2 104.2-33.6 160.9 27.1-56.2 40.2-110.1 29.3-160z'
fill={`url(#${be})`}
/>
<path
d='M312.5 311c53.1 19.9 73.6 63.6 88.9 100-19-38.1-45.2-80.3-90.8-96-34.8-11.8-64.1-10.4-114.3 1l-1.1-5c53.2-12.1 81-13.5 117.3 0z'
fill={`url(#${me})`}
/>
</g>
</g>
</svg>
)
}
export function NotionIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 50 50' width='1em' height='1em' {...props}>
@@ -1711,167 +1888,42 @@ export function StagehandIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
width='108'
height='159'
viewBox='0 0 108 159'
fill='none'
xmlns='http://www.w3.org/2000/svg'
width='256'
height='352'
viewBox='0 0 256 352'
fill='none'
>
<path
d='M15 26C22.8234 31.822 23.619 41.405 25.3125 50.3867C25.8461 53.1914 26.4211 55.9689 27.0625 58.75C27.7987 61.9868 28.4177 65.2319 29 68.5C29.332 70.3336 29.6653 72.1669 30 74C30.1418 74.7863 30.2836 75.5727 30.4297 76.3828C31.8011 83.2882 33.3851 90.5397 39.4375 94.75C40.3405 95.3069 40.3405 95.3069 41.2617 95.875C43.8517 97.5512 45.826 99.826 48 102C50.6705 102.89 52.3407 103.143 55.0898 103.211C55.8742 103.239 56.6586 103.268 57.4668 103.297C59.1098 103.349 60.7531 103.393 62.3965 103.43C65.8896 103.567 68.4123 103.705 71.5664 105.289C73 107 73 107 73 111C73.66 111 74.32 111 75 111C74.0759 106.912 74.0759 106.912 71.4766 103.828C67.0509 102.348 62.3634 102.64 57.7305 102.609C52.3632 102.449 49.2783 101.537 45 98C41.8212 94.0795 41.5303 90.9791 42 86C44.9846 83.0154 48.2994 83.6556 52.3047 83.6289C53.139 83.6199 53.9734 83.6108 54.833 83.6015C56.6067 83.587 58.3805 83.5782 60.1543 83.5745C62.8304 83.5627 65.5041 83.5137 68.1797 83.4629C81.1788 83.34 91.8042 85.3227 102 94C106.37 100.042 105.483 106.273 104.754 113.406C103.821 119.026 101.968 124.375 100.125 129.75C99.8806 130.471 99.6361 131.193 99.3843 131.936C97.7783 136.447 95.9466 140.206 93 144C92.34 144 91.68 144 91 144C91 144.66 91 145.32 91 146C79.0816 156.115 63.9798 156.979 49 156C36.6394 154.226 26.7567 148.879 19 139C11.0548 125.712 11.6846 105.465 11.3782 90.4719C11.0579 77.4745 8.03411 64.8142 5.4536 52.1135C5.04373 50.0912 4.64233 48.0673 4.24218 46.043C4.00354 44.8573 3.7649 43.6716 3.51903 42.45C2.14425 33.3121 2.14425 33.3121 4.87499 29.125C8.18297 25.817 10.3605 25.4542 15 26Z'
fill='#FDFDFD'
d='M 242.29,45.79 C 242.29,28.88 226.69,13.76 206.61,13.76 C 188.59,13.76 174.82,28.66 174.82,45.85 V 101.97 C 168.89,98.09 163.18,96.76 157.14,96.76 C 145.94,96.76 137.02,101.49 128.83,110.17 C 121.81,101.01 112.07,95.73 100.72,95.73 C 93.97,95.73 87.82,98.09 82.11,100.9 V 80.05 C 82.11,64.08 66.14,47.28 48.74,47.28 C 31.12,47.28 14.54,62.71 14.54,78.79 V 219.4 C 14.54,273.71 56.99,337.89 125.23,337.89 C 197.41,337.89 242.29,289.05 242.29,186.01 V 78.9 L 242.29,45.79 Z'
fill='black'
/>
<path
d='M91 0.999996C94.8466 2.96604 96.2332 5.08365 97.6091 9.03564C99.203 14.0664 99.4412 18.7459 99.4414 23.9922C99.4538 24.9285 99.4663 25.8647 99.4791 26.8294C99.5049 28.8198 99.5247 30.8103 99.539 32.8008C99.5785 37.9693 99.6682 43.1369 99.7578 48.3047C99.7747 49.3188 99.7917 50.3328 99.8091 51.3776C99.9603 59.6066 100.323 67.7921 100.937 76C101.012 77.0582 101.087 78.1163 101.164 79.2065C101.646 85.1097 102.203 90.3442 105.602 95.3672C107.492 98.9262 107.45 102.194 107.375 106.125C107.366 106.881 107.356 107.638 107.346 108.417C107.18 114.639 106.185 120.152 104 126C103.636 126.996 103.273 127.993 102.898 129.02C98.2182 141.022 92.6784 149.349 80.7891 155.062C67.479 160.366 49.4234 159.559 36 155C32.4272 153.286 29.2162 151.308 26 149C25.0719 148.361 24.1437 147.721 23.1875 147.062C8.32968 133.054 9.60387 109.231 8.73413 90.3208C8.32766 81.776 7.51814 73.4295 5.99999 65C5.82831 64.0338 5.65662 63.0675 5.47973 62.072C4.98196 59.3363 4.46395 56.6053 3.93749 53.875C3.76412 52.9572 3.59074 52.0394 3.4121 51.0938C2.75101 47.6388 2.11387 44.3416 0.999995 41C0.505898 36.899 0.0476353 32.7768 2.04687 29.0469C4.91881 25.5668 6.78357 24.117 11.25 23.6875C15.8364 24.0697 17.5999 24.9021 21 28C24.7763 34.3881 26.047 41.2626 27.1875 48.5C27.5111 50.4693 27.8377 52.4381 28.168 54.4062C28.3733 55.695 28.3733 55.695 28.5828 57.0098C28.8087 58.991 28.8087 58.991 30 60C30.3171 59.4947 30.6342 58.9894 30.9609 58.4688C33.1122 55.4736 34.7097 53.3284 38.3789 52.3945C44.352 52.203 48.1389 53.6183 53 57C53.0928 56.1338 53.0928 56.1338 53.1875 55.25C54.4089 51.8676 55.9015 50.8075 59 49C63.8651 48.104 66.9348 48.3122 71.1487 51.0332C72.0896 51.6822 73.0305 52.3313 74 53C73.9686 51.2986 73.9686 51.2986 73.9365 49.5627C73.8636 45.3192 73.818 41.0758 73.7803 36.8318C73.7603 35.0016 73.733 33.1715 73.6982 31.3415C73.6492 28.6976 73.6269 26.0545 73.6094 23.4102C73.5887 22.6035 73.5681 21.7969 73.5468 20.9658C73.5441 13.8444 75.5121 7.83341 80.25 2.4375C83.9645 0.495841 86.8954 0.209055 91 0.999996ZM3.99999 30C1.56925 34.8615 3.215 40.9393 4.24218 46.043C4.37061 46.6927 4.49905 47.3424 4.63137 48.0118C5.03968 50.0717 5.45687 52.1296 5.87499 54.1875C11.1768 80.6177 11.1768 80.6177 11.4375 93.375C11.7542 120.78 11.7542 120.78 23.5625 144.375C28.5565 149.002 33.5798 151.815 40 154C40.6922 154.244 41.3844 154.487 42.0977 154.738C55.6463 158.576 72.4909 156.79 84.8086 150.316C87.0103 148.994 89.0458 147.669 91 146C91 145.34 91 144.68 91 144C91.66 144 92.32 144 93 144C97.1202 138.98 99.3206 133.053 101.25 126.937C101.505 126.174 101.76 125.41 102.023 124.623C104.94 115.65 107.293 104.629 103.625 95.625C96.3369 88.3369 86.5231 83.6919 76.1988 83.6088C74.9905 83.6226 74.9905 83.6226 73.7578 83.6367C72.9082 83.6362 72.0586 83.6357 71.1833 83.6352C69.4034 83.6375 67.6235 83.6472 65.8437 83.6638C63.1117 83.6876 60.3806 83.6843 57.6484 83.6777C55.9141 83.6833 54.1797 83.6904 52.4453 83.6992C51.6277 83.6983 50.81 83.6974 49.9676 83.6964C45.5122 83.571 45.5122 83.571 42 86C41.517 90.1855 41.733 92.4858 43.6875 96.25C46.4096 99.4871 48.6807 101.674 53.0105 102.282C55.3425 102.411 57.6645 102.473 60 102.5C69.8847 102.612 69.8847 102.612 74 106C74.8125 108.687 74.8125 108.688 75 111C74.34 111 73.68 111 73 111C72.8969 110.216 72.7937 109.432 72.6875 108.625C72.224 105.67 72.224 105.67 69 104C65.2788 103.745 61.5953 103.634 57.8672 103.609C51.1596 103.409 46.859 101.691 41.875 97C41.2562 96.34 40.6375 95.68 40 95C39.175 94.4637 38.35 93.9275 37.5 93.375C30.9449 87.1477 30.3616 77.9789 29.4922 69.418C29.1557 66.1103 29.1557 66.1103 28.0352 63.625C26.5234 59.7915 26.1286 55.8785 25.5625 51.8125C23.9233 38.3 23.9233 38.3 17 27C11.7018 24.3509 7.9915 26.1225 3.99999 30Z'
fill='#1F1F1F'
d='M 224.94,46.23 C 224.94,36.76 215.91,28.66 205.91,28.66 C 196.75,28.66 189.9,36.11 189.9,45.14 V 152.72 C 202.88,153.38 214.08,155.96 224.94,166.19 V 78.79 L 224.94,46.23 Z'
fill='white'
/>
<path
d='M89.0976 2.53906C91 3 91 3 93.4375 5.3125C96.1586 9.99276 96.178 14.1126 96.2461 19.3828C96.2778 21.1137 96.3098 22.8446 96.342 24.5754C96.3574 25.4822 96.3728 26.3889 96.3887 27.3232C96.6322 41.3036 96.9728 55.2117 98.3396 69.1353C98.9824 75.7746 99.0977 82.3308 99 89C96.5041 88.0049 94.0126 87.0053 91.5351 85.9648C90.3112 85.4563 90.3112 85.4563 89.0625 84.9375C87.8424 84.4251 87.8424 84.4251 86.5976 83.9023C83.7463 82.9119 80.9774 82.4654 78 82C76.7702 65.9379 75.7895 49.8907 75.7004 33.7775C75.6919 32.3138 75.6783 30.8501 75.6594 29.3865C75.5553 20.4082 75.6056 12.1544 80.6875 4.4375C83.6031 2.62508 85.7 2.37456 89.0976 2.53906Z'
fill='#FBFBFB'
d='M 157.21,113.21 C 146.12,113.21 137.93,122.02 137.93,131.76 V 154.62 C 142.24,153.05 145.95,152.61 149.83,152.61 H 174.71 V 131.76 C 174.71,122.35 166.73,113.21 157.21,113.21 Z'
fill='white'
/>
<path
d='M97 13C97.99 13.495 97.99 13.495 99 14C99.0297 15.8781 99.0297 15.8781 99.0601 17.7942C99.4473 46.9184 99.4473 46.9184 100.937 76C101.012 77.0574 101.087 78.1149 101.164 79.2043C101.646 85.1082 102.203 90.3434 105.602 95.3672C107.492 98.9262 107.45 102.194 107.375 106.125C107.366 106.881 107.356 107.638 107.346 108.417C107.18 114.639 106.185 120.152 104 126C103.636 126.996 103.273 127.993 102.898 129.02C98.2182 141.022 92.6784 149.349 80.7891 155.062C67.479 160.366 49.4234 159.559 36 155C32.4272 153.286 29.2162 151.308 26 149C24.6078 148.041 24.6078 148.041 23.1875 147.062C13.5484 137.974 10.832 124.805 9.99999 112C9.91815 101.992 10.4358 91.9898 11 82C11.33 82 11.66 82 12 82C12.0146 82.6118 12.0292 83.2236 12.0442 83.854C11.5946 115.845 11.5946 115.845 24.0625 143.875C28.854 148.273 33.89 150.868 40 153C40.6935 153.245 41.387 153.49 42.1016 153.742C56.9033 157.914 73.8284 155.325 87 148C88.3301 147.327 89.6624 146.658 91 146C91 145.34 91 144.68 91 144C91.66 144 92.32 144 93 144C100.044 130.286 105.786 114.602 104 99C102.157 94.9722 100.121 93.0631 96.3125 90.875C95.5042 90.398 94.696 89.9211 93.8633 89.4297C85.199 85.1035 78.1558 84.4842 68.5 84.3125C67.2006 84.2783 65.9012 84.2442 64.5625 84.209C61.3751 84.127 58.1879 84.0577 55 84C55 83.67 55 83.34 55 83C58.9087 82.7294 62.8179 82.4974 66.7309 82.2981C68.7007 82.1902 70.6688 82.0535 72.6367 81.916C82.854 81.4233 90.4653 83.3102 99 89C98.8637 87.6094 98.8637 87.6094 98.7246 86.1907C96.96 67.8915 95.697 49.7051 95.75 31.3125C95.751 30.5016 95.7521 29.6908 95.7532 28.8554C95.7901 15.4198 95.7901 15.4198 97 13Z'
fill='#262114'
d='M 100.06,111.75 C 89.19,111.75 81.85,121.06 81.85,130.31 V 157.86 C 81.85,167.71 89.72,175.38 99.24,175.38 C 109.71,175.38 118.39,166.91 118.39,157.39 V 130.31 C 118.39,120.79 110.03,111.75 100.06,111.75 Z'
fill='white'
/>
<path
d='M68 51C72.86 54.06 74.644 56.5072 76 62C76.249 65.2763 76.2347 68.5285 76.1875 71.8125C76.1868 72.6833 76.1862 73.554 76.1855 74.4512C76.1406 80.8594 76.1406 80.8594 75 82C73.5113 82.0867 72.0185 82.107 70.5273 82.0976C69.6282 82.0944 68.7291 82.0912 67.8027 82.0879C66.8572 82.0795 65.9117 82.0711 64.9375 82.0625C63.9881 82.058 63.0387 82.0535 62.0605 82.0488C59.707 82.037 57.3535 82.0205 55 82C53.6352 77.2188 53.738 72.5029 53.6875 67.5625C53.6585 66.6208 53.6295 65.6792 53.5996 64.709C53.5591 60.2932 53.5488 57.7378 55.8945 53.9023C59.5767 50.5754 63.1766 50.211 68 51Z'
fill='#F8F8F8'
d='M 192.04,168.87 H 150.16 C 140.19,168.87 133.34,175.39 133.34,183.86 C 133.34,192.9 140.19,199.75 148.66,199.75 H 182.52 C 188.01,199.75 189.63,204.81 189.63,207.49 C 189.63,211.91 186.37,214.64 181.09,215.51 C 162.96,218.66 137.71,229.13 137.71,259.68 C 137.71,265.07 133.67,267.42 130.29,267.42 C 126.09,267.42 122.38,264.74 122.38,260.12 C 122.38,241.15 129.02,228.17 143.26,214.81 C 131.01,212.02 119.21,202.99 117.75,186.43 C 111.93,189.81 107.2,191.15 100.18,191.15 C 82.11,191.15 66.68,176.58 66.68,158.29 V 80.71 C 66.68,71.24 57.16,63.5 49.18,63.5 C 38.71,63.5 29.89,72.42 29.89,80.27 V 217.19 C 29.89,266.48 68.71,322.19 124.88,322.19 C 185.91,322.19 223.91,282.15 223.91,207.16 C 223.91,187.19 214.28,168.87 192.04,168.87 Z'
fill='white'
/>
</svg>
)
}
export function BrandfetchIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 29 31' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M46 55C48.7557 57.1816 50.4359 58.8718 52 62C52.0837 63.5215 52.1073 65.0466 52.0977 66.5703C52.0944 67.4662 52.0912 68.3621 52.0879 69.2852C52.0795 70.2223 52.0711 71.1595 52.0625 72.125C52.058 73.0699 52.0535 74.0148 52.0488 74.9883C52.037 77.3256 52.0206 79.6628 52 82C50.9346 82.1992 50.9346 82.1992 49.8477 82.4023C48.9286 82.5789 48.0094 82.7555 47.0625 82.9375C46.146 83.1115 45.2294 83.2855 44.2852 83.4648C42.0471 83.7771 42.0471 83.7771 41 85C40.7692 86.3475 40.5885 87.7038 40.4375 89.0625C40.2931 90.3619 40.1487 91.6613 40 93C37 92 37 92 35.8672 90.1094C35.5398 89.3308 35.2123 88.5522 34.875 87.75C34.5424 86.9817 34.2098 86.2134 33.8672 85.4219C31.9715 80.1277 31.7884 75.065 31.75 69.5C31.7294 68.7536 31.7087 68.0073 31.6875 67.2383C31.6551 62.6607 32.0474 59.7266 35 56C38.4726 54.2637 42.2119 54.3981 46 55Z'
fill='#FAFAFA'
/>
<path
d='M97 13C97.66 13.33 98.32 13.66 99 14C99.0297 15.8781 99.0297 15.8781 99.0601 17.7942C99.4473 46.9184 99.4473 46.9184 100.937 76C101.012 77.0574 101.087 78.1149 101.164 79.2043C101.566 84.1265 102.275 88.3364 104 93C103.625 95.375 103.625 95.375 103 97C102.361 96.2781 101.721 95.5563 101.062 94.8125C94.4402 88.1902 85.5236 84.8401 76.2891 84.5859C75.0451 84.5473 73.8012 84.5086 72.5195 84.4688C71.2343 84.4378 69.9491 84.4069 68.625 84.375C66.6624 84.317 66.6624 84.317 64.6601 84.2578C61.4402 84.1638 58.2203 84.0781 55 84C55 83.67 55 83.34 55 83C58.9087 82.7294 62.8179 82.4974 66.7309 82.2981C68.7007 82.1902 70.6688 82.0535 72.6367 81.916C82.854 81.4233 90.4653 83.3102 99 89C98.9091 88.0729 98.8182 87.1458 98.7246 86.1907C96.96 67.8915 95.697 49.7051 95.75 31.3125C95.751 30.5016 95.7521 29.6908 95.7532 28.8554C95.7901 15.4198 95.7901 15.4198 97 13Z'
fill='#423B28'
/>
<path
d='M91 0.999996C94.3999 3.06951 96.8587 5.11957 98 9C97.625 12.25 97.625 12.25 97 15C95.804 12.6081 94.6146 10.2139 93.4375 7.8125C92.265 5.16236 92.265 5.16236 91 4C88.074 3.7122 85.8483 3.51695 83 4C79.1128 7.37574 78.178 11.0991 77 16C76.8329 18.5621 76.7615 21.1317 76.7695 23.6992C76.77 24.4155 76.7704 25.1318 76.7709 25.8698C76.7739 27.3783 76.7817 28.8868 76.7942 30.3953C76.8123 32.664 76.8147 34.9324 76.8144 37.2012C76.8329 44.6001 77.0765 51.888 77.7795 59.259C78.1413 63.7564 78.1068 68.2413 78.0625 72.75C78.058 73.6498 78.0535 74.5495 78.0488 75.4766C78.0373 77.6511 78.0193 79.8255 78 82C78.99 82.495 78.99 82.495 80 83C68.78 83.33 57.56 83.66 46 84C46.495 83.01 46.495 83.01 47 82C52.9349 80.7196 58.8909 80.8838 64.9375 80.9375C65.9075 80.942 66.8775 80.9465 67.8769 80.9512C70.2514 80.9629 72.6256 80.9793 75 81C75.0544 77.9997 75.0939 75.0005 75.125 72C75.1418 71.1608 75.1585 70.3216 75.1758 69.457C75.2185 63.9475 74.555 59.2895 73 54C73.66 54 74.32 54 75 54C74.9314 53.2211 74.8629 52.4422 74.7922 51.6396C74.1158 43.5036 73.7568 35.4131 73.6875 27.25C73.644 25.5194 73.644 25.5194 73.5996 23.7539C73.5376 15.3866 74.6189 8.85069 80.25 2.4375C83.9433 0.506911 86.9162 0.173322 91 0.999996Z'
fill='#131311'
/>
<path
d='M15 24C20.2332 26.3601 22.1726 29.3732 24.1875 34.5195C26.8667 42.6988 27.2651 50.4282 27 59C26.67 59 26.34 59 26 59C25.8945 58.436 25.7891 57.8721 25.6804 57.291C25.1901 54.6926 24.6889 52.0963 24.1875 49.5C24.0218 48.6131 23.8562 47.7262 23.6855 46.8125C21.7568 35.5689 21.7568 35.5689 15 27C12.0431 26.2498 12.0431 26.2498 8.99999 27C5.97965 28.9369 5.97965 28.9369 3.99999 32C3.67226 36.9682 4.31774 41.4911 5.27733 46.3594C5.40814 47.0304 5.53894 47.7015 5.67371 48.3929C5.94892 49.7985 6.22723 51.2035 6.50854 52.6079C6.93887 54.7569 7.35989 56.9075 7.77929 59.0586C9.09359 66.104 9.09359 66.104 11 73C11.0836 75.2109 11.1073 77.4243 11.0976 79.6367C11.0944 80.9354 11.0912 82.2342 11.0879 83.5723C11.0795 84.944 11.0711 86.3158 11.0625 87.6875C11.0575 89.071 11.0529 90.4544 11.0488 91.8379C11.037 95.2253 11.0206 98.6126 11 102C8.54975 99.5498 8.73228 98.8194 8.65624 95.4492C8.62812 94.53 8.60001 93.6108 8.57104 92.6638C8.54759 91.6816 8.52415 90.6994 8.49999 89.6875C8.20265 81.3063 7.58164 73.2485 5.99999 65C5.67135 63.2175 5.34327 61.435 5.01562 59.6523C4.31985 55.9098 3.62013 52.1681 2.90233 48.4297C2.75272 47.6484 2.60311 46.867 2.44897 46.062C1.99909 43.8187 1.99909 43.8187 0.999995 41C0.505898 36.899 0.0476353 32.7768 2.04687 29.0469C6.06003 24.1839 8.81126 23.4843 15 24Z'
fill='#2A2311'
/>
<path
d='M11 82C11.33 82 11.66 82 12 82C12.0146 82.6118 12.0292 83.2236 12.0442 83.854C11.5946 115.845 11.5946 115.845 24.0625 143.875C30.0569 149.404 36.9894 152.617 45 154C42 156 42 156 39.4375 156C29.964 153.244 20.8381 146.677 16 138C8.26993 120.062 9.92611 101.014 11 82Z'
fill='#272214'
/>
<path
d='M68 49C70.3478 50.1116 71.9703 51.3346 74 53C73.34 53.66 72.68 54.32 72 55C71.505 54.505 71.01 54.01 70.5 53.5C67.6718 51.8031 65.3662 51.5622 62.0976 51.4062C58.4026 52.4521 57.1992 53.8264 55 57C54.3826 61.2861 54.5302 65.4938 54.6875 69.8125C54.7101 70.9823 54.7326 72.1521 54.7559 73.3574C54.8147 76.2396 54.8968 79.1191 55 82C54.01 82 53.02 82 52 82C51.9854 81.4203 51.9708 80.8407 51.9558 80.2434C51.881 77.5991 51.7845 74.9561 51.6875 72.3125C51.6649 71.4005 51.6424 70.4885 51.6191 69.5488C51.4223 64.6292 51.2621 60.9548 48 57C45.6603 55.8302 44.1661 55.8339 41.5625 55.8125C40.78 55.7983 39.9976 55.7841 39.1914 55.7695C36.7079 55.8591 36.7079 55.8591 34 58C32.7955 60.5518 32.7955 60.5518 32 63C31.34 63 30.68 63 30 63C30.2839 59.6879 31.0332 57.9518 32.9375 55.1875C36.7018 52.4987 38.9555 52.3484 43.4844 52.5586C47.3251 53.2325 49.8148 54.7842 53 57C53.0928 56.1338 53.0928 56.1338 53.1875 55.25C55.6091 48.544 61.7788 47.8649 68 49Z'
fill='#1F1A0F'
/>
<path
d='M99 60C99.33 60 99.66 60 100 60C100.05 60.7865 100.1 61.573 100.152 62.3833C100.385 65.9645 100.63 69.5447 100.875 73.125C100.954 74.3625 101.032 75.6 101.113 76.875C101.197 78.0738 101.281 79.2727 101.367 80.5078C101.44 81.6075 101.514 82.7073 101.589 83.8403C102.013 87.1 102.94 89.8988 104 93C103.625 95.375 103.625 95.375 103 97C102.361 96.2781 101.721 95.5563 101.062 94.8125C94.4402 88.1902 85.5236 84.8401 76.2891 84.5859C74.4231 84.5279 74.4231 84.5279 72.5195 84.4688C71.2343 84.4378 69.9491 84.4069 68.625 84.375C67.3166 84.3363 66.0082 84.2977 64.6601 84.2578C61.4402 84.1638 58.2203 84.0781 55 84C55 83.67 55 83.34 55 83C58.9087 82.7294 62.8179 82.4974 66.7309 82.2981C68.7007 82.1902 70.6688 82.0535 72.6367 81.916C82.854 81.4233 90.4653 83.3102 99 89C98.9162 87.912 98.8324 86.8241 98.7461 85.7031C98.1266 77.012 97.9127 68.6814 99 60Z'
fill='#332E22'
/>
<path
d='M15 24C20.2332 26.3601 22.1726 29.3732 24.1875 34.5195C26.8667 42.6988 27.2651 50.4282 27 59C26.67 59 26.34 59 26 59C25.8945 58.436 25.7891 57.8721 25.6804 57.291C25.1901 54.6926 24.6889 52.0963 24.1875 49.5C24.0218 48.6131 23.8562 47.7262 23.6855 46.8125C21.7568 35.5689 21.7568 35.5689 15 27C12.0431 26.2498 12.0431 26.2498 8.99999 27C5.2818 29.7267 4.15499 31.2727 3.18749 35.8125C3.12562 36.8644 3.06374 37.9163 2.99999 39C2.33999 39 1.67999 39 0.999992 39C0.330349 31.2321 0.330349 31.2321 3.37499 27.5625C7.31431 23.717 9.51597 23.543 15 24Z'
fill='#1D180A'
/>
<path
d='M91 0.999996C94.3999 3.06951 96.8587 5.11957 98 9C97.625 12.25 97.625 12.25 97 15C95.804 12.6081 94.6146 10.2139 93.4375 7.8125C92.265 5.16236 92.265 5.16236 91 4C85.4345 3.33492 85.4345 3.33491 80.6875 5.75C78.5543 9.85841 77.6475 13.9354 76.7109 18.4531C76.4763 19.2936 76.2417 20.1341 76 21C75.34 21.33 74.68 21.66 74 22C73.5207 15.4102 74.5846 10.6998 78 5C81.755 0.723465 85.5463 -0.103998 91 0.999996Z'
fill='#16130D'
/>
<path
d='M42 93C42.5569 93.7631 43.1137 94.5263 43.6875 95.3125C46.4238 98.4926 48.7165 100.679 53.0105 101.282C55.3425 101.411 57.6646 101.473 60 101.5C70.6207 101.621 70.6207 101.621 75 106C75.0406 107.666 75.0427 109.334 75 111C74.34 111 73.68 111 73 111C72.7112 110.196 72.4225 109.391 72.125 108.562C71.2674 105.867 71.2674 105.867 69 105C65.3044 104.833 61.615 104.703 57.916 104.658C52.1631 104.454 48.7484 103.292 44 100C41.5625 97.25 41.5625 97.25 40 95C40.66 95 41.32 95 42 95C42 94.34 42 93.68 42 93Z'
fill='#2B2B2B'
/>
<path
d='M11 82C11.33 82 11.66 82 12 82C12.1682 86.6079 12.3287 91.216 12.4822 95.8245C12.5354 97.3909 12.5907 98.9574 12.6482 100.524C12.7306 102.78 12.8055 105.036 12.8789 107.293C12.9059 107.989 12.933 108.685 12.9608 109.402C13.0731 113.092 12.9015 116.415 12 120C11.67 120 11.34 120 11 120C9.63778 112.17 10.1119 104.4 10.4375 96.5C10.4908 95.0912 10.5436 93.6823 10.5957 92.2734C10.7247 88.8487 10.8596 85.4243 11 82Z'
fill='#4D483B'
/>
<path
d='M43.4844 52.5586C47.3251 53.2325 49.8148 54.7842 53 57C52 59 52 59 50 60C49.5256 59.34 49.0512 58.68 48.5625 58C45.2656 55.4268 43.184 55.5955 39.1211 55.6641C36.7043 55.8955 36.7043 55.8955 34 58C32.7955 60.5518 32.7955 60.5518 32 63C31.34 63 30.68 63 30 63C30.2839 59.6879 31.0332 57.9518 32.9375 55.1875C36.7018 52.4987 38.9555 52.3484 43.4844 52.5586Z'
fill='#221F16'
/>
<path
d='M76 73C76.33 73 76.66 73 77 73C77 75.97 77 78.94 77 82C78.485 82.495 78.485 82.495 80 83C68.78 83.33 57.56 83.66 46 84C46.33 83.34 46.66 82.68 47 82C52.9349 80.7196 58.8909 80.8838 64.9375 80.9375C65.9075 80.942 66.8775 80.9465 67.8769 80.9512C70.2514 80.9629 72.6256 80.9793 75 81C75.33 78.36 75.66 75.72 76 73Z'
fill='#040404'
/>
<path
d='M27 54C27.33 54 27.66 54 28 54C28.33 56.97 28.66 59.94 29 63C29.99 63 30.98 63 32 63C32 66.96 32 70.92 32 75C31.01 74.67 30.02 74.34 29 74C28.8672 73.2523 28.7344 72.5047 28.5977 71.7344C28.421 70.7495 28.2444 69.7647 28.0625 68.75C27.8885 67.7755 27.7144 66.8009 27.5352 65.7969C27.0533 63.087 27.0533 63.087 26.4062 60.8125C25.8547 58.3515 26.3956 56.4176 27 54Z'
fill='#434039'
/>
<path
d='M78 5C78.99 5.33 79.98 5.66 81 6C80.3194 6.92812 80.3194 6.92812 79.625 7.875C77.7233 11.532 77.1555 14.8461 76.5273 18.8906C76.3533 19.5867 76.1793 20.2828 76 21C75.34 21.33 74.68 21.66 74 22C73.5126 15.2987 74.9229 10.9344 78 5Z'
fill='#2A2313'
/>
<path
d='M12 115C12.99 115.495 12.99 115.495 14 116C14.5334 118.483 14.9326 120.864 15.25 123.375C15.3531 124.061 15.4562 124.747 15.5625 125.453C16.0763 129.337 16.2441 130.634 14 134C12.6761 127.57 11.752 121.571 12 115Z'
fill='#2F2C22'
/>
<path
d='M104 95C107 98 107 98 107.363 101.031C107.347 102.176 107.33 103.321 107.312 104.5C107.309 105.645 107.305 106.789 107.301 107.969C107 111 107 111 105 114C104.67 107.73 104.34 101.46 104 95Z'
fill='#120F05'
/>
<path
d='M56 103C58.6048 102.919 61.2071 102.86 63.8125 102.812C64.5505 102.787 65.2885 102.762 66.0488 102.736C71.4975 102.662 71.4975 102.662 74 104.344C75.374 106.619 75.2112 108.396 75 111C74.34 111 73.68 111 73 111C72.7112 110.196 72.4225 109.391 72.125 108.562C71.2674 105.867 71.2674 105.867 69 105C66.7956 104.77 64.5861 104.589 62.375 104.438C61.1865 104.354 59.998 104.27 58.7734 104.184C57.4006 104.093 57.4006 104.093 56 104C56 103.67 56 103.34 56 103Z'
fill='#101010'
/>
<path
d='M23 40C23.66 40 24.32 40 25 40C27.3084 46.3482 27.1982 52.2948 27 59C26.67 59 26.34 59 26 59C25.01 52.73 24.02 46.46 23 40Z'
fill='#191409'
/>
<path
d='M47 83C46.3606 83.3094 45.7212 83.6187 45.0625 83.9375C41.9023 87.0977 42.181 90.6833 42 95C41.01 94.67 40.02 94.34 39 94C39.3463 85.7409 39.3463 85.7409 41.875 82.875C44 82 44 82 47 83Z'
fill='#171717'
/>
<path
d='M53 61C53.33 61 53.66 61 54 61C54.33 67.93 54.66 74.86 55 82C54.01 82 53.02 82 52 82C52.33 75.07 52.66 68.14 53 61Z'
fill='#444444'
/>
<path
d='M81 154C78.6696 156.33 77.8129 156.39 74.625 156.75C73.4687 156.897 73.4687 156.897 72.2891 157.047C69.6838 156.994 68.2195 156.317 66 155C67.7478 154.635 69.4984 154.284 71.25 153.938C72.7118 153.642 72.7118 153.642 74.2031 153.34C76.8681 153.016 78.4887 153.145 81 154Z'
fill='#332F23'
/>
<path
d='M19 28C19.66 28 20.32 28 21 28C21.6735 29.4343 22.3386 30.8726 23 32.3125C23.5569 33.5133 23.5569 33.5133 24.125 34.7383C25 37 25 37 25 40C22 39 22 39 21.0508 37.2578C20.8071 36.554 20.5635 35.8502 20.3125 35.125C20.0611 34.4263 19.8098 33.7277 19.5508 33.0078C19 31 19 31 19 28Z'
fill='#282213'
/>
<path
d='M102 87C104.429 93.2857 104.429 93.2857 103 97C100.437 94.75 100.437 94.75 98 92C98.0625 89.75 98.0625 89.75 99 88C101 87 101 87 102 87Z'
fill='#37301F'
/>
<path
d='M53 56C53.33 56 53.66 56 54 56C53.67 62.27 53.34 68.54 53 75C52.67 75 52.34 75 52 75C51.7788 72.2088 51.5726 69.4179 51.375 66.625C51.3105 65.8309 51.2461 65.0369 51.1797 64.2188C51.0394 62.1497 51.0124 60.0737 51 58C51.66 57.34 52.32 56.68 53 56Z'
fill='#030303'
/>
<path
d='M100 129C100.33 129 100.66 129 101 129C100.532 133.776 99.7567 137.045 97 141C96.34 140.67 95.68 140.34 95 140C96.65 136.37 98.3 132.74 100 129Z'
fill='#1E1A12'
/>
<path
d='M15 131C17.7061 132.353 17.9618 133.81 19.125 136.562C19.4782 137.389 19.8314 138.215 20.1953 139.066C20.4609 139.704 20.7264 140.343 21 141C20.01 141 19.02 141 18 141C15.9656 137.27 15 135.331 15 131Z'
fill='#1C1912'
/>
<path
d='M63 49C69.4 49.4923 69.4 49.4923 72.4375 52.0625C73.2109 53.0216 73.2109 53.0216 74 54C70.8039 54 69.5828 53.4533 66.8125 52C66.0971 51.6288 65.3816 51.2575 64.6445 50.875C64.1018 50.5863 63.5591 50.2975 63 50C63 49.67 63 49.34 63 49Z'
fill='#13110C'
/>
<path
d='M0.999992 39C1.98999 39 2.97999 39 3.99999 39C5.24999 46.625 5.24999 46.625 2.99999 50C2.33999 46.37 1.67999 42.74 0.999992 39Z'
fill='#312C1E'
/>
<path
d='M94 5C94.66 5 95.32 5 96 5C97.8041 7.75924 98.0127 8.88972 97.625 12.25C97.4187 13.1575 97.2125 14.065 97 15C95.1161 11.7345 94.5071 8.71888 94 5Z'
fill='#292417'
/>
<path
d='M20 141C23.3672 142.393 24.9859 143.979 27 147C24.625 146.812 24.625 146.812 22 146C20.6875 143.438 20.6875 143.438 20 141Z'
fill='#373328'
/>
<path
d='M86 83C86.33 83.99 86.66 84.98 87 86C83.37 85.34 79.74 84.68 76 84C80.3553 81.8223 81.4663 81.9696 86 83Z'
fill='#2F2F2F'
/>
<path
d='M42 93C46 97.625 46 97.625 46 101C44.02 99.35 42.04 97.7 40 96C40.66 95.67 41.32 95.34 42 95C42 94.34 42 93.68 42 93Z'
fill='#232323'
/>
<path
d='M34 55C34.66 55.33 35.32 55.66 36 56C35.5256 56.7838 35.0512 57.5675 34.5625 58.375C33.661 59.8895 32.7882 61.4236 32 63C31.34 63 30.68 63 30 63C30.4983 59.3125 31.1007 57.3951 34 55Z'
fill='#110F0A'
d='M29 7.54605C29 9.47222 28.316 11.1378 26.9481 12.5428C25.5802 13.9251 23.5852 14.9222 20.9634 15.534C22.377 15.9192 23.4484 16.5537 24.1781 17.4375C24.9077 18.2987 25.2724 19.2956 25.2724 20.4287C25.2724 22.2189 24.7025 23.7713 23.5625 25.0855C22.4454 26.3998 20.8039 27.4195 18.638 28.1447C16.4721 28.8472 13.8616 29.1985 10.8066 29.1985C9.66666 29.1985 8.75472 29.1645 8.07075 29.0965C8.04796 29.7309 7.77438 30.2068 7.25 30.5241C6.72562 30.8414 6.05307 31 5.23231 31C4.41156 31 3.84159 30.8187 3.52241 30.4561C3.22603 30.0936 3.10062 29.561 3.14623 28.8586C3.35141 25.686 3.75039 22.3662 4.34316 18.8991C4.93593 15.4094 5.68829 12.0442 6.60024 8.80373C6.75982 8.23721 7.07901 7.84064 7.55778 7.61404C8.03656 7.38743 8.66353 7.27412 9.43868 7.27412C10.8294 7.27412 11.5248 7.65936 11.5248 8.42983C11.5248 8.74708 11.4564 9.10965 11.3196 9.51754C10.7268 11.2851 10.134 13.6871 9.54127 16.7237C8.9485 19.7375 8.52674 22.6156 8.27594 25.3575C9.37028 25.448 10.2594 25.4934 10.9434 25.4934C14.1352 25.4934 16.4721 25.0401 17.954 24.1338C19.4587 23.2046 20.2111 22.0263 20.2111 20.5987C20.2111 19.6016 19.778 18.7632 18.9116 18.0833C18.0681 17.4035 16.6431 17.0296 14.6368 16.9616C14.1808 16.939 13.8616 16.8257 13.6792 16.6217C13.4968 16.4178 13.4057 16.0892 13.4057 15.636C13.4057 14.9788 13.5425 14.4463 13.816 14.0384C14.0896 13.6305 14.5912 13.4152 15.3208 13.3925C16.9395 13.3472 18.3986 13.1093 19.6981 12.6787C21.0204 12.2482 22.0578 11.6477 22.8101 10.8772C23.5625 10.0841 23.9387 9.1663 23.9387 8.1239C23.9387 6.80958 23.2889 5.77851 21.9894 5.0307C20.6899 4.26024 18.6949 3.875 16.0047 3.875C13.5652 3.875 11.2056 4.19226 8.92571 4.82676C6.64584 5.4386 4.70793 6.2204 3.11203 7.17215C2.38246 7.6027 1.7669 7.81798 1.26533 7.81798C0.854953 7.81798 0.53577 7.68202 0.307783 7.41009C0.102594 7.1155 0 6.75292 0 6.32237C0 5.75585 0.113994 5.26864 0.341981 4.86075C0.592768 4.45285 1.17414 3.98831 2.08608 3.46711C4.00118 2.37939 6.24685 1.52961 8.82311 0.917763C11.3994 0.305921 14.0326 0 16.7229 0C20.8494 0 23.9272 0.691156 25.9564 2.07347C27.9855 3.45577 29 5.27998 29 7.54605Z'
fill='currentColor'
/>
</svg>
)
@@ -1931,6 +1983,14 @@ export function Mem0Icon(props: SVGProps<SVGSVGElement>) {
)
}
export function EvernoteIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='#7fce2c'>
<path d='M29.343 16.818c.1 1.695-.08 3.368-.305 5.045-.225 1.712-.508 3.416-.964 5.084-.3 1.067-.673 2.1-1.202 3.074-.65 1.192-1.635 1.87-2.992 1.924l-3.832.036c-.636-.017-1.278-.146-1.9-.297-1.192-.3-1.862-1.1-2.06-2.3-.186-1.08-.173-2.187.04-3.264.252-1.23 1-1.96 2.234-2.103.817-.1 1.65-.077 2.476-.1.205-.007.275.098.203.287-.196.53-.236 1.07-.098 1.623.053.207-.023.307-.26.305a7.77 7.77 0 0 0-1.123.053c-.636.086-.96.47-.96 1.112 0 .205.026.416.066.622.103.507.45.78.944.837 1.123.127 2.247.138 3.37-.05.675-.114 1.08-.54 1.16-1.208.152-1.3.155-2.587-.228-3.845-.33-1.092-1.006-1.565-2.134-1.7l-3.36-.54c-1.06-.193-1.7-.887-1.92-1.9-.13-.572-.14-1.17-.214-1.757-.013-.106-.074-.208-.1-.3-.04.1-.106.212-.117.326-.066.68-.053 1.373-.185 2.04-.16.8-.404 1.566-.67 2.33-.185.535-.616.837-1.205.8a37.76 37.76 0 0 1-7.123-1.353l-.64-.207c-.927-.26-1.487-.903-1.74-1.787l-1-3.853-.74-4.3c-.115-.755-.2-1.523-.083-2.293.154-1.112.914-1.903 2.04-1.964l3.558-.062c.127 0 .254.003.373-.026a1.23 1.23 0 0 0 1.01-1.255l-.05-3.036c-.048-1.576.8-2.38 2.156-2.622a10.58 10.58 0 0 1 4.91.26c.933.275 1.467.923 1.715 1.83.058.22.146.3.37.287l2.582.01 3.333.37c.686.095 1.364.25 2.032.42 1.165.298 1.793 1.112 1.962 2.256l.357 3.355.3 5.577.01 2.277zm-4.534-1.155c-.02-.666-.07-1.267-.444-1.784a1.66 1.66 0 0 0-2.469-.15c-.364.4-.494.88-.564 1.4-.008.034.106.126.16.126l.8-.053c.768.007 1.523.113 2.25.393.066.026.136.04.265.077zM8.787 1.154a3.82 3.82 0 0 0-.278 1.592l.05 2.934c.005.357-.075.45-.433.45L5.1 6.156c-.583 0-1.143.1-1.554.278l5.2-5.332c.02.013.04.033.06.053z' />
</svg>
)
}
export function ElevenLabsIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
@@ -2018,7 +2078,7 @@ export function LangsmithIcon(props: SVGProps<SVGSVGElement>) {
export function LemlistIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 180 181' fill='none'>
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='24 24.92 132 132' fill='none'>
<path
fillRule='evenodd'
clipRule='evenodd'
@@ -2467,7 +2527,7 @@ export function PagerDutyIcon(props: SVGProps<SVGSVGElement>) {
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64' fill='none'>
<path
d='M6.704 59.217H0v-33.65c0-3.455 1.418-5.544 2.604-6.704 2.63-2.58 6.2-2.656 6.782-2.656h10.546c3.765 0 5.93 1.52 7.117 2.8 2.346 2.553 2.372 5.853 2.32 6.73v12.687c0 3.662-1.496 5.828-2.733 6.988-2.553 2.398-5.93 2.45-6.73 2.424H6.704zm13.46-18.102c.36 0 1.367-.103 1.908-.62.413-.387.62-1.083.62-2.1v-13.02c0-.36-.077-1.315-.593-1.857-.5-.516-1.444-.62-2.166-.62h-10.6c-2.63 0-2.63 1.985-2.63 2.656v15.55zM57.296 4.783H64V38.46c0 3.455-1.418 5.544-2.604 6.704-2.63 2.58-6.2 2.656-6.782 2.656H44.068c-3.765 0-5.93-1.52-7.117-2.8-2.346-2.553-2.372-5.853-2.32-6.73V25.62c0-3.662 1.496-5.828 2.733-6.988 2.553-2.398 5.93-2.45 6.73-2.424h13.202zM43.836 22.9c-.36 0-1.367.103-1.908.62-.413.387-.62 1.083-.62 2.1v13.02c0 .36.077 1.315.593 1.857.5.516 1.444.62 2.166.62h10.598c2.656-.026 2.656-2 2.656-2.682V22.9z'
fill='#06AC38'
fill='#FFFFFF'
/>
</svg>
)
@@ -4796,6 +4856,22 @@ export function GoogleGroupsIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function GoogleMeetIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 87.5 72'>
<path fill='#00832d' d='M49.5 36l8.53 9.75 11.47 7.33 2-17.02-2-16.64-11.69 6.44z' />
<path fill='#0066da' d='M0 51.5V66c0 3.315 2.685 6 6 6h14.5l3-10.96-3-9.54-9.95-3z' />
<path fill='#e94235' d='M20.5 0L0 20.5l10.55 3 9.95-3 2.95-9.41z' />
<path fill='#2684fc' d='M20.5 20.5H0v31h20.5z' />
<path
fill='#00ac47'
d='M82.6 8.68L69.5 19.42v33.66l13.16 10.79c1.97 1.54 4.85.135 4.85-2.37V11c0-2.535-2.945-3.925-4.91-2.32zM49.5 36v15.5h-29V72h43c3.315 0 6-2.685 6-6V53.08z'
/>
<path fill='#ffba00' d='M63.5 0h-43v20.5h29V36l20-16.57V6c0-3.315-2.685-6-6-6z' />
</svg>
)
}
export function CursorIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 546 546' fill='currentColor'>
@@ -4804,6 +4880,19 @@ export function CursorIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function DubIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 64 64' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M32 64c17.673 0 32-14.327 32-32 0-11.844-6.435-22.186-16-27.719V48h-8v-2.14A15.9 15.9 0 0 1 32 48c-8.837 0-16-7.163-16-16s7.163-16 16-16c2.914 0 5.647.78 8 2.14V1.008A32 32 0 0 0 32 0C14.327 0 0 14.327 0 32s14.327 32 32 32'
fill='currentColor'
/>
</svg>
)
}
export function DuckDuckGoIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='-108 -108 216 216'>

View File

@@ -74,7 +74,7 @@ export function StructuredData({
name: 'Sim Documentation',
url: baseUrl,
description:
'Comprehensive documentation for Sim visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines.',
'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.',
publisher: {
'@type': 'Organization',
name: 'Sim',
@@ -98,7 +98,7 @@ export function StructuredData({
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Any',
description:
'Visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines by connecting blocks on a canvas—no coding required.',
'Sim is 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. Create agents, workflows, knowledge bases, tables, and docs.',
url: baseUrl,
author: {
'@type': 'Organization',
@@ -109,12 +109,13 @@ export function StructuredData({
category: 'Developer Tools',
},
featureList: [
'Visual workflow builder with drag-and-drop interface',
'AI agent creation and automation',
'80+ built-in integrations',
'Real-time team collaboration',
'Multiple deployment options',
'Custom integrations via MCP protocol',
'AI agent creation',
'Agentic workflow orchestration',
'1,000+ integrations',
'LLM orchestration (OpenAI, Anthropic, Google, xAI, Mistral, Perplexity)',
'Knowledge base creation',
'Table creation',
'Document creation',
],
}

View File

@@ -17,6 +17,7 @@ import {
AshbyIcon,
AttioIcon,
BrainIcon,
BrandfetchIcon,
BrowserUseIcon,
CalComIcon,
CalendlyIcon,
@@ -33,11 +34,13 @@ import {
DocumentIcon,
DropboxIcon,
DsPyIcon,
DubIcon,
DuckDuckGoIcon,
DynamoDBIcon,
ElasticsearchIcon,
ElevenLabsIcon,
EnrichSoIcon,
EvernoteIcon,
ExaAIIcon,
EyeIcon,
FirecrawlIcon,
@@ -57,6 +60,7 @@ import {
GoogleGroupsIcon,
GoogleIcon,
GoogleMapsIcon,
GoogleMeetIcon,
GooglePagespeedIcon,
GoogleSheetsIcon,
GoogleSlidesIcon,
@@ -100,6 +104,7 @@ import {
MySQLIcon,
Neo4jIcon,
NotionIcon,
ObsidianIcon,
OnePasswordIcon,
OpenAIIcon,
OutlookIcon,
@@ -177,6 +182,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
asana: AsanaIcon,
ashby: AshbyIcon,
attio: AttioIcon,
brandfetch: BrandfetchIcon,
browser_use: BrowserUseIcon,
calcom: CalComIcon,
calendly: CalendlyIcon,
@@ -192,11 +198,13 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
discord: DiscordIcon,
dropbox: DropboxIcon,
dspy: DsPyIcon,
dub: DubIcon,
duckduckgo: DuckDuckGoIcon,
dynamodb: DynamoDBIcon,
elasticsearch: ElasticsearchIcon,
elevenlabs: ElevenLabsIcon,
enrich: EnrichSoIcon,
evernote: EvernoteIcon,
exa: ExaAIIcon,
file_v3: DocumentIcon,
firecrawl: FirecrawlIcon,
@@ -215,6 +223,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
google_forms: GoogleFormsIcon,
google_groups: GoogleGroupsIcon,
google_maps: GoogleMapsIcon,
google_meet: GoogleMeetIcon,
google_pagespeed: GooglePagespeedIcon,
google_search: GoogleIcon,
google_sheets_v2: GoogleSheetsIcon,
@@ -259,6 +268,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
mysql: MySQLIcon,
neo4j: Neo4jIcon,
notion_v2: NotionIcon,
obsidian: ObsidianIcon,
onedrive: MicrosoftOneDriveIcon,
onepassword: OnePasswordIcon,
openai: OpenAIIcon,

View File

@@ -11,6 +11,8 @@
"(generated)/workflows",
"(generated)/logs",
"(generated)/usage",
"(generated)/audit-logs"
"(generated)/audit-logs",
"(generated)/tables",
"(generated)/files"
]
}

View File

@@ -190,13 +190,8 @@ console.log(`${processedItems} gültige Elemente verarbeitet`);
### Einschränkungen
<Callout type="warning">
Container-Blöcke (Schleifen und Parallele) können nicht ineinander verschachtelt werden. Das bedeutet:
- Du kannst keinen Schleifenblock in einen anderen Schleifenblock platzieren
- Du kannst keinen Parallel-Block in einen Schleifenblock platzieren
- Du kannst keinen Container-Block in einen anderen Container-Block platzieren
Wenn du mehrdimensionale Iterationen benötigst, erwäge eine Umstrukturierung deines Workflows, um sequentielle Schleifen zu verwenden oder Daten in Stufen zu verarbeiten.
<Callout type="info">
Container-Blöcke (Schleifen und Parallele) unterstützen Verschachtelung. Du kannst Schleifen in Schleifen, Parallele in Schleifen und jede Kombination von Container-Blöcken platzieren, um komplexe mehrdimensionale Workflows zu erstellen.
</Callout>
<Callout type="info">

View File

@@ -142,11 +142,8 @@ Jede parallele Instanz läuft unabhängig:
### Einschränkungen
<Callout type="warning">
Container-Blöcke (Schleifen und Parallele) können nicht ineinander verschachtelt werden. Das bedeutet:
- Sie können keinen Schleifenblock in einen Parallelblock platzieren
- Sie können keinen weiteren Parallelblock in einen Parallelblock platzieren
- Sie können keinen Container-Block in einen anderen Container-Block platzieren
<Callout type="info">
Container-Blöcke (Schleifen und Parallele) unterstützen Verschachtelung. Sie können Parallele in Parallele, Schleifen in Parallele und jede Kombination von Container-Blöcken platzieren, um komplexe mehrdimensionale Workflows zu erstellen.
</Callout>
<Callout type="info">

View File

@@ -11,6 +11,9 @@
"(generated)/workflows",
"(generated)/logs",
"(generated)/usage",
"(generated)/audit-logs"
"(generated)/audit-logs",
"(generated)/tables",
"(generated)/files",
"(generated)/knowledge-bases"
]
}

View File

@@ -184,13 +184,8 @@ Variables (i=0) → Loop (While i<10) → Agent (Process) → Variables (i++)
### Limitations
<Callout type="warning">
Container blocks (Loops and Parallels) cannot be nested inside each other. This means:
- You cannot place a Loop block inside another Loop block
- You cannot place a Parallel block inside a Loop block
- You cannot place any container block inside another container block
If you need multi-dimensional iteration, consider restructuring your workflow to use sequential loops or process data in stages.
<Callout type="info">
Container blocks (Loops and Parallels) support nesting. You can place loops inside loops, parallels inside loops, and any combination of container blocks to build complex multi-dimensional workflows.
</Callout>
<Callout type="info">

View File

@@ -148,11 +148,8 @@ Each parallel instance runs independently:
### Limitations
<Callout type="warning">
Container blocks (Loops and Parallels) cannot be nested inside each other. This means:
- You cannot place a Loop block inside a Parallel block
- You cannot place another Parallel block inside a Parallel block
- You cannot place any container block inside another container block
<Callout type="info">
Container blocks (Loops and Parallels) support nesting. You can place parallels inside parallels, loops inside parallels, and any combination of container blocks to build complex multi-dimensional workflows.
</Callout>
<Callout type="info">

View File

@@ -0,0 +1,143 @@
---
title: Connectors
description: Automatically sync documents from external sources into your knowledge base
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Step, Steps } from 'fumadocs-ui/components/steps'
Connectors let you pull documents directly from external services into your knowledge base. Instead of manually uploading files, a connector continuously syncs content from sources like Notion, Google Drive, GitHub, Slack, and more — keeping your knowledge base up to date automatically.
## Available Connectors
Sim ships with 20+ built-in connectors spanning productivity tools, cloud storage, development platforms, and more.
| Category | Connectors |
|----------|-----------|
| **Productivity** | Notion, Confluence, Asana, Linear, Jira |
| **Cloud Storage** | Google Drive, Dropbox, OneDrive, SharePoint |
| **Documents** | Google Docs, WordPress, Webflow |
| **Development** | GitHub |
| **Communication** | Slack |
| **CRM** | HubSpot, Salesforce |
| **Data** | Airtable |
| **Note-taking** | Evernote, Obsidian |
| **Meetings** | Fireflies |
## Adding a Connector
<Steps>
<Step>
### Select a source
Open a knowledge base and click **Add Connector**. You'll see the full list of available connectors — pick the service you want to sync from.
</Step>
<Step>
### Authenticate
Most connectors use **OAuth** — select an existing credential from the dropdown, or click **Connect new account** to authorize through the service's login flow. Tokens are refreshed automatically, so you won't need to re-authenticate unless you revoke access.
A few connectors (Evernote, Obsidian, Fireflies) use **API keys** instead. Paste your key or developer token directly, and it will be stored securely.
<Callout type="info">
If you rotate an API key in the external service, you'll need to update it in Sim as well. OAuth tokens are refreshed automatically, but API keys are not.
</Callout>
</Step>
<Step>
### Configure
Each connector has its own configuration fields that control what gets synced. Some examples:
- **Notion**: Choose between syncing an entire workspace, a specific database, or a single page tree
- **GitHub**: Specify a repository, branch, and optional file extension filter
- **Confluence**: Enter your Atlassian domain and optionally filter by space key or content type
- **Obsidian**: Provide your vault URL and optionally restrict to a folder path
All configuration is validated when you save — if a repository doesn't exist or a domain is unreachable, you'll get an immediate error.
</Step>
<Step>
### Choose sync frequency
Select how often the connector should re-sync:
| Frequency | Description |
|-----------|-------------|
| Every hour | Best for fast-moving sources |
| Every 6 hours | Good balance for most use cases |
| **Daily** (default) | Suitable for content that changes infrequently |
| Weekly | For stable, rarely-updated sources |
| Manual only | Sync only when you trigger it |
</Step>
<Step>
### Configure metadata tags (optional)
If the connector supports metadata tags, you'll see checkboxes for each tag type (e.g., Labels, Last Modified, Notebook). All are enabled by default — uncheck any you don't need.
See the [Metadata Tags](#metadata-tags) section below for details.
</Step>
<Step>
### Connect & Sync
Click **Connect & Sync** to save the connector and trigger the first sync immediately. Documents will begin appearing in your knowledge base as they are processed.
</Step>
</Steps>
## How Syncing Works
On each sync, the connector fetches documents from the external service and compares them against what's already in your knowledge base. Only documents that have actually changed are reprocessed — new content is added, updated content is re-chunked and re-embedded, and documents that no longer exist in the source are removed.
This means syncing is efficient even for large document sets. A connector with thousands of documents will only do meaningful work when something changes.
### Handling Failures
If a single document fails to fetch (e.g., due to a permission issue or timeout), the sync continues with the remaining documents. The failed document will be retried on the next sync cycle.
If an entire sync fails (e.g., the service is down or credentials expired), the connector automatically backs off and retries later. The backoff resets as soon as a sync succeeds.
## Metadata Tags
Connectors can automatically populate [tags](/docs/knowledgebase/tags) with metadata from the source, letting you filter documents in the Knowledge block based on information from the external service.
For example, a Notion connector might tag documents with their **Labels**, **Last Modified** date, and **Created** date. A GitHub connector might tag documents with their **Repository** and **File Path**. This metadata becomes available for [tag-based filtering](/docs/knowledgebase/tags) in your workflows.
### Opting Out
You can disable specific metadata tags during connector setup. Disabled tags won't be populated, leaving those tag slots available for other connectors or manual tagging.
<Callout type="info">
Tag slots are shared across all documents in a knowledge base. If you have multiple connectors, each one's metadata tags draw from the same pool of available slots.
</Callout>
## Excluding Documents
You can manually exclude specific documents from a connector's sync. Excluded documents are skipped on every subsequent sync, even if they change in the source. This is useful for filtering out templates, drafts, or other content you don't want in your knowledge base.
## Source Links
Every synced document retains a link back to the original in the external service. This lets you trace any knowledge base document to its source — whether that's a Notion page, a GitHub file, a Confluence article, or a Slack conversation.
## Multiple Connectors
You can add multiple connectors to a single knowledge base. For example, you might sync internal documentation from Confluence alongside code from GitHub and meeting notes from Fireflies — all searchable together through the Knowledge block.
Each connector manages its own documents independently. Metadata tag slots are shared across the knowledge base, so keep an eye on slot usage if you're combining several connectors that each populate tags.
## Common Use Cases
- **Internal knowledge base**: Sync your team's Notion workspace and Confluence spaces so AI agents can answer questions about internal processes, policies, and documentation
- **Customer support**: Connect HubSpot or Salesforce alongside your help docs from WordPress or Google Docs to give support agents full context on customers and product information
- **Engineering assistant**: Sync a GitHub repository and Jira or Linear issues so an AI agent can reference code, specs, and ticket history when answering developer questions
- **Meeting intelligence**: Pull in Fireflies transcripts alongside Slack conversations to build a searchable archive of decisions and discussions
- **Research and notes**: Sync Evernote notebooks or an Obsidian vault to make your personal notes available to AI workflows

View File

@@ -1,4 +1,4 @@
{
"title": "Knowledgebase",
"pages": ["index", "tags"]
"pages": ["index", "connectors", "tags"]
}

View File

@@ -26,12 +26,63 @@ In Sim, the Airtable integration enables your agents to interact with your Airta
## Usage Instructions
Integrates Airtable into the workflow. Can create, get, list, or update Airtable records. Can be used in trigger mode to trigger a workflow when an update is made to an Airtable table.
Integrates Airtable into the workflow. Can list bases, list tables (with schema), and create, get, list, or update records. Can also be used in trigger mode to trigger a workflow when an update is made to an Airtable table.
## Tools
### `airtable_list_bases`
List all bases the authenticated user has access to
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `offset` | string | No | Pagination offset for retrieving additional bases |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `bases` | array | Array of Airtable bases with id, name, and permissionLevel |
| ↳ `id` | string | Base ID \(starts with "app"\) |
| ↳ `name` | string | Base name |
| ↳ `permissionLevel` | string | Permission level \(none, read, comment, edit, create\) |
| `metadata` | json | Pagination and count metadata |
| ↳ `offset` | string | Offset for next page of results |
| ↳ `totalBases` | number | Number of bases returned |
### `airtable_list_tables`
List all tables and their schema in an Airtable base
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `baseId` | string | Yes | Airtable base ID \(starts with "app", e.g., "appXXXXXXXXXXXXXX"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `tables` | array | List of tables in the base with their schema |
| ↳ `id` | string | Table ID \(starts with "tbl"\) |
| ↳ `name` | string | Table name |
| ↳ `description` | string | Table description |
| ↳ `primaryFieldId` | string | ID of the primary field |
| ↳ `fields` | array | List of fields in the table |
| ↳ `id` | string | Field ID \(starts with "fld"\) |
| ↳ `name` | string | Field name |
| ↳ `type` | string | Field type \(singleLineText, multilineText, number, checkbox, singleSelect, multipleSelects, date, dateTime, attachment, linkedRecord, etc.\) |
| ↳ `description` | string | Field description |
| ↳ `options` | json | Field-specific options \(choices, etc.\) |
| `metadata` | json | Base info and count metadata |
| ↳ `baseId` | string | The base ID queried |
| ↳ `totalTables` | number | Number of tables in the base |
### `airtable_list_records`
Read records from an Airtable table
@@ -49,8 +100,13 @@ Read records from an Airtable table
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `records` | json | Array of retrieved Airtable records |
| `records` | array | Array of retrieved Airtable records |
| ↳ `id` | string | Record ID |
| ↳ `createdTime` | string | Record creation timestamp |
| ↳ `fields` | json | Record field values |
| `metadata` | json | Operation metadata including pagination offset and total records count |
| ↳ `offset` | string | Pagination offset for next page |
| ↳ `totalRecords` | number | Number of records returned |
### `airtable_get_record`
@@ -68,8 +124,12 @@ Retrieve a single record from an Airtable table by its ID
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `record` | json | Retrieved Airtable record with id, createdTime, and fields |
| `metadata` | json | Operation metadata including record count |
| `record` | json | Retrieved Airtable record |
| ↳ `id` | string | Record ID |
| ↳ `createdTime` | string | Record creation timestamp |
| ↳ `fields` | json | Record field values |
| `metadata` | json | Operation metadata |
| ↳ `recordCount` | number | Number of records returned \(always 1\) |
### `airtable_create_records`
@@ -88,8 +148,12 @@ Write new records to an Airtable table
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `records` | json | Array of created Airtable records |
| `records` | array | Array of created Airtable records |
| ↳ `id` | string | Record ID |
| ↳ `createdTime` | string | Record creation timestamp |
| ↳ `fields` | json | Record field values |
| `metadata` | json | Operation metadata |
| ↳ `recordCount` | number | Number of records created |
### `airtable_update_record`
@@ -108,8 +172,13 @@ Update an existing record in an Airtable table by ID
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `record` | json | Updated Airtable record with id, createdTime, and fields |
| `metadata` | json | Operation metadata including record count and updated field names |
| `record` | json | Updated Airtable record |
| ↳ `id` | string | Record ID |
| ↳ `createdTime` | string | Record creation timestamp |
| ↳ `fields` | json | Record field values |
| `metadata` | json | Operation metadata |
| ↳ `recordCount` | number | Number of records updated \(always 1\) |
| ↳ `updatedFields` | array | List of field names that were updated |
### `airtable_update_multiple_records`
@@ -127,7 +196,29 @@ Update multiple existing records in an Airtable table
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `records` | json | Array of updated Airtable records |
| `metadata` | json | Operation metadata including record count and updated record IDs |
| `records` | array | Array of updated Airtable records |
| ↳ `id` | string | Record ID |
| ↳ `createdTime` | string | Record creation timestamp |
| ↳ `fields` | json | Record field values |
| `metadata` | json | Operation metadata |
| ↳ `recordCount` | number | Number of records updated |
| ↳ `updatedRecordIds` | array | List of updated record IDs |
### `airtable_get_base_schema`
Get the schema of all tables, fields, and views in an Airtable base
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `baseId` | string | Yes | Airtable base ID \(starts with "app", e.g., "appXXXXXXXXXXXXXX"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `tables` | json | Array of table schemas with fields and views |
| `metadata` | json | Operation metadata including total tables count |

View File

@@ -0,0 +1,83 @@
---
title: Brandfetch
description: Look up brand assets, logos, colors, and company info
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="brandfetch"
color="#000000"
/>
## Usage Instructions
Integrate Brandfetch into your workflow. Retrieve brand logos, colors, fonts, and company data by domain, ticker, or name search.
## Tools
### `brandfetch_get_brand`
Retrieve brand assets including logos, colors, fonts, and company info by domain, ticker, ISIN, or crypto symbol
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Brandfetch API key |
| `identifier` | string | Yes | Brand identifier: domain \(nike.com\), stock ticker \(NKE\), ISIN \(US6541061031\), or crypto symbol \(BTC\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Unique brand identifier |
| `name` | string | Brand name |
| `domain` | string | Brand domain |
| `claimed` | boolean | Whether the brand profile is claimed |
| `description` | string | Short brand description |
| `longDescription` | string | Detailed brand description |
| `links` | array | Social media and website links |
| ↳ `name` | string | Link name \(e.g., twitter, linkedin\) |
| ↳ `url` | string | Link URL |
| `logos` | array | Brand logos with formats and themes |
| ↳ `type` | string | Logo type \(logo, icon, symbol, other\) |
| ↳ `theme` | string | Logo theme \(light, dark\) |
| ↳ `formats` | array | Available formats with src URL, format, width, and height |
| `colors` | array | Brand colors with hex values and types |
| ↳ `hex` | string | Hex color code |
| ↳ `type` | string | Color type \(accent, dark, light, brand\) |
| ↳ `brightness` | number | Brightness value |
| `fonts` | array | Brand fonts with names and types |
| ↳ `name` | string | Font name |
| ↳ `type` | string | Font type \(title, body\) |
| ↳ `origin` | string | Font origin \(google, custom, system\) |
| `company` | json | Company firmographic data including employees, location, and industries |
| `qualityScore` | number | Data quality score from 0 to 1 |
| `isNsfw` | boolean | Whether the brand contains adult content |
### `brandfetch_search`
Search for brands by name and find their domains and logos
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Brandfetch API key |
| `name` | string | Yes | Company or brand name to search for |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | array | List of matching brands |
| ↳ `brandId` | string | Unique brand identifier |
| ↳ `name` | string | Brand name |
| ↳ `domain` | string | Brand domain |
| ↳ `claimed` | boolean | Whether the brand profile is claimed |
| ↳ `icon` | string | Brand icon URL |

View File

@@ -0,0 +1,318 @@
---
title: Dub
description: Link management with Dub
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="dub"
color="#181C1E"
/>
{/* MANUAL-CONTENT-START:intro */}
[Dub](https://dub.co/) is an open-source link management platform for modern marketing teams. It provides powerful short link creation, analytics, and tracking capabilities with enterprise-grade infrastructure.
With the Dub integration in Sim, you can:
- **Create short links**: Generate branded short links with custom domains, slugs, and UTM parameters
- **Upsert links**: Create or update links idempotently by destination URL
- **Retrieve link info**: Look up link details by ID, external ID, or domain + key combination
- **Update links**: Modify destination URLs, metadata, UTM parameters, and link settings
- **Delete links**: Remove short links by ID or external ID
- **List links**: Search and filter links with pagination, sorting, and tag filtering
- **Get analytics**: Retrieve click, lead, and sales analytics with grouping by time, geography, device, browser, referer, and more
In Sim, the Dub integration enables your agents to manage short links and track their performance programmatically. Use it to create trackable links as part of marketing workflows, monitor link engagement, and make data-driven decisions based on click analytics and conversion metrics.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Create, manage, and track short links with Dub. Supports custom domains, UTM parameters, link analytics, and more.
## Tools
### `dub_create_link`
Create a new short link with Dub. Supports custom domains, slugs, UTM parameters, and more.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Dub API key |
| `url` | string | Yes | The destination URL of the short link |
| `domain` | string | No | Custom domain for the short link \(defaults to dub.sh\) |
| `key` | string | No | Custom slug for the short link \(randomly generated if not provided\) |
| `externalId` | string | No | External ID for the link in your database |
| `tagIds` | string | No | Comma-separated tag IDs to assign to the link |
| `comments` | string | No | Comments for the short link |
| `expiresAt` | string | No | Expiration date in ISO 8601 format |
| `password` | string | No | Password to protect the short link |
| `rewrite` | boolean | No | Whether to enable link cloaking |
| `archived` | boolean | No | Whether to archive the link |
| `title` | string | No | Custom OG title for the link preview |
| `description` | string | No | Custom OG description for the link preview |
| `utm_source` | string | No | UTM source parameter |
| `utm_medium` | string | No | UTM medium parameter |
| `utm_campaign` | string | No | UTM campaign parameter |
| `utm_term` | string | No | UTM term parameter |
| `utm_content` | string | No | UTM content parameter |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Unique ID of the created link |
| `domain` | string | Domain of the short link |
| `key` | string | Slug of the short link |
| `url` | string | Destination URL |
| `shortLink` | string | Full short link URL |
| `qrCode` | string | QR code URL for the short link |
| `archived` | boolean | Whether the link is archived |
| `externalId` | string | External ID |
| `title` | string | OG title |
| `description` | string | OG description |
| `tags` | json | Tags assigned to the link \(id, name, color\) |
| `clicks` | number | Number of clicks |
| `leads` | number | Number of leads |
| `sales` | number | Number of sales |
| `saleAmount` | number | Total sale amount in cents |
| `lastClicked` | string | Last clicked timestamp |
| `createdAt` | string | Creation timestamp |
| `updatedAt` | string | Last update timestamp |
| `utm_source` | string | UTM source parameter |
| `utm_medium` | string | UTM medium parameter |
| `utm_campaign` | string | UTM campaign parameter |
| `utm_term` | string | UTM term parameter |
| `utm_content` | string | UTM content parameter |
### `dub_upsert_link`
Create or update a short link by its URL. If a link with the same URL already exists, update it. Otherwise, create a new link.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Dub API key |
| `url` | string | Yes | The destination URL of the short link |
| `domain` | string | No | Custom domain for the short link \(defaults to dub.sh\) |
| `key` | string | No | Custom slug for the short link \(randomly generated if not provided\) |
| `externalId` | string | No | External ID for the link in your database |
| `tagIds` | string | No | Comma-separated tag IDs to assign to the link |
| `comments` | string | No | Comments for the short link |
| `expiresAt` | string | No | Expiration date in ISO 8601 format |
| `password` | string | No | Password to protect the short link |
| `rewrite` | boolean | No | Whether to enable link cloaking |
| `archived` | boolean | No | Whether to archive the link |
| `title` | string | No | Custom OG title for the link preview |
| `description` | string | No | Custom OG description for the link preview |
| `utm_source` | string | No | UTM source parameter |
| `utm_medium` | string | No | UTM medium parameter |
| `utm_campaign` | string | No | UTM campaign parameter |
| `utm_term` | string | No | UTM term parameter |
| `utm_content` | string | No | UTM content parameter |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Unique ID of the link |
| `domain` | string | Domain of the short link |
| `key` | string | Slug of the short link |
| `url` | string | Destination URL |
| `shortLink` | string | Full short link URL |
| `qrCode` | string | QR code URL for the short link |
| `archived` | boolean | Whether the link is archived |
| `externalId` | string | External ID |
| `title` | string | OG title |
| `description` | string | OG description |
| `tags` | json | Tags assigned to the link \(id, name, color\) |
| `clicks` | number | Number of clicks |
| `leads` | number | Number of leads |
| `sales` | number | Number of sales |
| `saleAmount` | number | Total sale amount in cents |
| `lastClicked` | string | Last clicked timestamp |
| `createdAt` | string | Creation timestamp |
| `updatedAt` | string | Last update timestamp |
| `utm_source` | string | UTM source parameter |
| `utm_medium` | string | UTM medium parameter |
| `utm_campaign` | string | UTM campaign parameter |
| `utm_term` | string | UTM term parameter |
| `utm_content` | string | UTM content parameter |
### `dub_get_link`
Retrieve information about a short link by its link ID, external ID, or domain + key combination.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Dub API key |
| `linkId` | string | No | The unique ID of the short link |
| `externalId` | string | No | The external ID of the link in your database |
| `domain` | string | No | The domain of the link \(use with key\) |
| `key` | string | No | The slug of the link \(use with domain\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Unique ID of the link |
| `domain` | string | Domain of the short link |
| `key` | string | Slug of the short link |
| `url` | string | Destination URL |
| `shortLink` | string | Full short link URL |
| `qrCode` | string | QR code URL for the short link |
| `archived` | boolean | Whether the link is archived |
| `externalId` | string | External ID |
| `title` | string | OG title |
| `description` | string | OG description |
| `tags` | json | Tags assigned to the link \(id, name, color\) |
| `clicks` | number | Number of clicks |
| `leads` | number | Number of leads |
| `sales` | number | Number of sales |
| `saleAmount` | number | Total sale amount in cents |
| `lastClicked` | string | Last clicked timestamp |
| `createdAt` | string | Creation timestamp |
| `updatedAt` | string | Last update timestamp |
| `utm_source` | string | UTM source parameter |
| `utm_medium` | string | UTM medium parameter |
| `utm_campaign` | string | UTM campaign parameter |
| `utm_term` | string | UTM term parameter |
| `utm_content` | string | UTM content parameter |
### `dub_update_link`
Update an existing short link. You can modify the destination URL, slug, metadata, UTM parameters, and more.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Dub API key |
| `linkId` | string | Yes | The link ID or external ID prefixed with ext_ |
| `url` | string | No | New destination URL |
| `domain` | string | No | New custom domain |
| `key` | string | No | New custom slug |
| `title` | string | No | Custom OG title |
| `description` | string | No | Custom OG description |
| `externalId` | string | No | External ID for the link |
| `tagIds` | string | No | Comma-separated tag IDs |
| `comments` | string | No | Comments for the short link |
| `expiresAt` | string | No | Expiration date in ISO 8601 format |
| `password` | string | No | Password to protect the link |
| `rewrite` | boolean | No | Whether to enable link cloaking |
| `archived` | boolean | No | Whether to archive the link |
| `utm_source` | string | No | UTM source parameter |
| `utm_medium` | string | No | UTM medium parameter |
| `utm_campaign` | string | No | UTM campaign parameter |
| `utm_term` | string | No | UTM term parameter |
| `utm_content` | string | No | UTM content parameter |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Unique ID of the updated link |
| `domain` | string | Domain of the short link |
| `key` | string | Slug of the short link |
| `url` | string | Destination URL |
| `shortLink` | string | Full short link URL |
| `qrCode` | string | QR code URL for the short link |
| `archived` | boolean | Whether the link is archived |
| `externalId` | string | External ID |
| `title` | string | OG title |
| `description` | string | OG description |
| `tags` | json | Tags assigned to the link \(id, name, color\) |
| `clicks` | number | Number of clicks |
| `leads` | number | Number of leads |
| `sales` | number | Number of sales |
| `saleAmount` | number | Total sale amount in cents |
| `lastClicked` | string | Last clicked timestamp |
| `createdAt` | string | Creation timestamp |
| `updatedAt` | string | Last update timestamp |
| `utm_source` | string | UTM source parameter |
| `utm_medium` | string | UTM medium parameter |
| `utm_campaign` | string | UTM campaign parameter |
| `utm_term` | string | UTM term parameter |
| `utm_content` | string | UTM content parameter |
### `dub_delete_link`
Delete a short link by its link ID or external ID (prefixed with ext_).
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Dub API key |
| `linkId` | string | Yes | The link ID or external ID prefixed with ext_ |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | ID of the deleted link |
### `dub_list_links`
Retrieve a paginated list of short links for the authenticated workspace. Supports filtering by domain, search query, tags, and sorting.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Dub API key |
| `domain` | string | No | Filter by domain |
| `search` | string | No | Search query matched against the short link slug and destination URL |
| `tagIds` | string | No | Comma-separated tag IDs to filter by |
| `showArchived` | boolean | No | Whether to include archived links \(defaults to false\) |
| `sortBy` | string | No | Sort by field: createdAt, clicks, saleAmount, or lastClicked |
| `sortOrder` | string | No | Sort order: asc or desc |
| `page` | number | No | Page number \(default: 1\) |
| `pageSize` | number | No | Number of links per page \(default: 100, max: 100\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `links` | json | Array of link objects \(id, domain, key, url, shortLink, clicks, tags, createdAt\) |
| `count` | number | Number of links returned |
### `dub_get_analytics`
Retrieve analytics for links including clicks, leads, and sales. Supports filtering by link, time range, and grouping by various dimensions.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Dub API key |
| `event` | string | No | Event type: clicks \(default\), leads, sales, or composite |
| `groupBy` | string | No | Group results by: count \(default\), timeseries, countries, cities, devices, browsers, os, referers, top_links, top_urls |
| `linkId` | string | No | Filter by link ID |
| `externalId` | string | No | Filter by external ID \(prefix with ext_\) |
| `domain` | string | No | Filter by domain |
| `interval` | string | No | Time interval: 24h \(default\), 7d, 30d, 90d, 1y, mtd, qtd, ytd, or all |
| `start` | string | No | Start date/time in ISO 8601 format \(overrides interval\) |
| `end` | string | No | End date/time in ISO 8601 format \(defaults to now\) |
| `country` | string | No | Filter by country \(ISO 3166-1 alpha-2 code\) |
| `timezone` | string | No | IANA timezone for timeseries data \(defaults to UTC\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `clicks` | number | Total number of clicks |
| `leads` | number | Total number of leads |
| `sales` | number | Total number of sales |
| `saleAmount` | number | Total sale amount in cents |
| `data` | json | Grouped analytics data \(timeseries, countries, devices, etc.\) |

View File

@@ -0,0 +1,267 @@
---
title: Evernote
description: Manage notes, notebooks, and tags in Evernote
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="evernote"
color="#E0E0E0"
/>
## Usage Instructions
Integrate with Evernote to manage notes, notebooks, and tags. Create, read, update, copy, search, and delete notes. Create and list notebooks and tags.
## Tools
### `evernote_copy_note`
Copy a note to another notebook in Evernote
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Evernote developer token |
| `noteGuid` | string | Yes | GUID of the note to copy |
| `toNotebookGuid` | string | Yes | GUID of the destination notebook |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `note` | object | The copied note metadata |
| ↳ `guid` | string | New note GUID |
| ↳ `title` | string | Note title |
| ↳ `notebookGuid` | string | GUID of the destination notebook |
| ↳ `created` | number | Creation timestamp in milliseconds |
| ↳ `updated` | number | Last updated timestamp in milliseconds |
### `evernote_create_note`
Create a new note in Evernote
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Evernote developer token |
| `title` | string | Yes | Title of the note |
| `content` | string | Yes | Content of the note \(plain text or ENML\) |
| `notebookGuid` | string | No | GUID of the notebook to create the note in \(defaults to default notebook\) |
| `tagNames` | string | No | Comma-separated list of tag names to apply |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `note` | object | The created note |
| ↳ `guid` | string | Unique identifier of the note |
| ↳ `title` | string | Title of the note |
| ↳ `content` | string | ENML content of the note |
| ↳ `notebookGuid` | string | GUID of the containing notebook |
| ↳ `tagNames` | array | Tag names applied to the note |
| ↳ `created` | number | Creation timestamp in milliseconds |
| ↳ `updated` | number | Last updated timestamp in milliseconds |
### `evernote_create_notebook`
Create a new notebook in Evernote
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Evernote developer token |
| `name` | string | Yes | Name for the new notebook |
| `stack` | string | No | Stack name to group the notebook under |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `notebook` | object | The created notebook |
| ↳ `guid` | string | Notebook GUID |
| ↳ `name` | string | Notebook name |
| ↳ `defaultNotebook` | boolean | Whether this is the default notebook |
| ↳ `serviceCreated` | number | Creation timestamp in milliseconds |
| ↳ `serviceUpdated` | number | Last updated timestamp in milliseconds |
| ↳ `stack` | string | Notebook stack name |
### `evernote_create_tag`
Create a new tag in Evernote
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Evernote developer token |
| `name` | string | Yes | Name for the new tag |
| `parentGuid` | string | No | GUID of the parent tag for hierarchy |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `tag` | object | The created tag |
| ↳ `guid` | string | Tag GUID |
| ↳ `name` | string | Tag name |
| ↳ `parentGuid` | string | Parent tag GUID |
| ↳ `updateSequenceNum` | number | Update sequence number |
### `evernote_delete_note`
Move a note to the trash in Evernote
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Evernote developer token |
| `noteGuid` | string | Yes | GUID of the note to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the note was successfully deleted |
| `noteGuid` | string | GUID of the deleted note |
### `evernote_get_note`
Retrieve a note from Evernote by its GUID
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Evernote developer token |
| `noteGuid` | string | Yes | GUID of the note to retrieve |
| `withContent` | boolean | No | Whether to include note content \(default: true\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `note` | object | The retrieved note |
| ↳ `guid` | string | Unique identifier of the note |
| ↳ `title` | string | Title of the note |
| ↳ `content` | string | ENML content of the note |
| ↳ `contentLength` | number | Length of the note content |
| ↳ `notebookGuid` | string | GUID of the containing notebook |
| ↳ `tagGuids` | array | GUIDs of tags on the note |
| ↳ `tagNames` | array | Names of tags on the note |
| ↳ `created` | number | Creation timestamp in milliseconds |
| ↳ `updated` | number | Last updated timestamp in milliseconds |
| ↳ `active` | boolean | Whether the note is active \(not in trash\) |
### `evernote_get_notebook`
Retrieve a notebook from Evernote by its GUID
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Evernote developer token |
| `notebookGuid` | string | Yes | GUID of the notebook to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `notebook` | object | The retrieved notebook |
| ↳ `guid` | string | Notebook GUID |
| ↳ `name` | string | Notebook name |
| ↳ `defaultNotebook` | boolean | Whether this is the default notebook |
| ↳ `serviceCreated` | number | Creation timestamp in milliseconds |
| ↳ `serviceUpdated` | number | Last updated timestamp in milliseconds |
| ↳ `stack` | string | Notebook stack name |
### `evernote_list_notebooks`
List all notebooks in an Evernote account
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Evernote developer token |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `notebooks` | array | List of notebooks |
### `evernote_list_tags`
List all tags in an Evernote account
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Evernote developer token |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `tags` | array | List of tags |
### `evernote_search_notes`
Search for notes in Evernote using the Evernote search grammar
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Evernote developer token |
| `query` | string | Yes | Search query using Evernote search grammar \(e.g., "tag:work intitle:meeting"\) |
| `notebookGuid` | string | No | Restrict search to a specific notebook by GUID |
| `offset` | number | No | Starting index for results \(default: 0\) |
| `maxNotes` | number | No | Maximum number of notes to return \(default: 25\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalNotes` | number | Total number of matching notes |
| `notes` | array | List of matching note metadata |
### `evernote_update_note`
Update an existing note in Evernote
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Evernote developer token |
| `noteGuid` | string | Yes | GUID of the note to update |
| `title` | string | No | New title for the note |
| `content` | string | No | New content for the note \(plain text or ENML\) |
| `notebookGuid` | string | No | GUID of the notebook to move the note to |
| `tagNames` | string | No | Comma-separated list of tag names \(replaces existing tags\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `note` | object | The updated note |
| ↳ `guid` | string | Unique identifier of the note |
| ↳ `title` | string | Title of the note |
| ↳ `content` | string | ENML content of the note |
| ↳ `notebookGuid` | string | GUID of the containing notebook |
| ↳ `tagNames` | array | Tag names on the note |
| ↳ `created` | number | Creation timestamp in milliseconds |
| ↳ `updated` | number | Last updated timestamp in milliseconds |

View File

@@ -55,6 +55,9 @@ Search the web using Exa AI. Returns relevant search results with titles, URLs,
| `summary` | boolean | No | Include AI-generated summaries in results \(default: false\) |
| `livecrawl` | string | No | Live crawling mode: never \(default\), fallback, always, or preferred \(always try livecrawl, fall back to cache if fails\) |
| `apiKey` | string | Yes | Exa AI API Key |
| `pricing` | custom | No | No description |
| `metadata` | string | No | No description |
| `rateLimit` | string | No | No description |
#### Output
@@ -87,6 +90,9 @@ Retrieve the contents of webpages using Exa AI. Returns the title, text content,
| `highlights` | boolean | No | Include highlighted snippets in results \(default: false\) |
| `livecrawl` | string | No | Live crawling mode: never \(default\), fallback, always, or preferred \(always try livecrawl, fall back to cache if fails\) |
| `apiKey` | string | Yes | Exa AI API Key |
| `pricing` | custom | No | No description |
| `metadata` | string | No | No description |
| `rateLimit` | string | No | No description |
#### Output
@@ -116,6 +122,9 @@ Find webpages similar to a given URL using Exa AI. Returns a list of similar lin
| `summary` | boolean | No | Include AI-generated summaries in results \(default: false\) |
| `livecrawl` | string | No | Live crawling mode: never \(default\), fallback, always, or preferred \(always try livecrawl, fall back to cache if fails\) |
| `apiKey` | string | Yes | Exa AI API Key |
| `pricing` | custom | No | No description |
| `metadata` | string | No | No description |
| `rateLimit` | string | No | No description |
#### Output
@@ -138,6 +147,9 @@ Get an AI-generated answer to a question with citations from the web using Exa A
| `query` | string | Yes | The question to answer |
| `text` | boolean | No | Whether to include the full text of the answer |
| `apiKey` | string | Yes | Exa AI API Key |
| `pricing` | custom | No | No description |
| `metadata` | string | No | No description |
| `rateLimit` | string | No | No description |
#### Output

View File

@@ -0,0 +1,156 @@
---
title: Google Meet
description: Create and manage Google Meet meetings
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="google_meet"
color="#E0E0E0"
/>
{/* MANUAL-CONTENT-START:intro */}
[Google Meet](https://meet.google.com) is Google's video conferencing and online meeting platform, providing secure, high-quality video calls for individuals and teams. As a core component of Google Workspace, Google Meet enables real-time collaboration through video meetings, screen sharing, and integrated chat.
The Google Meet REST API (v2) allows programmatic management of meeting spaces and conference records, enabling automated workflows to create meetings, track participation, and manage active conferences without manual intervention.
Key features of the Google Meet API include:
- **Meeting Space Management**: Create, retrieve, and configure meeting spaces with customizable access controls.
- **Conference Records**: Access historical conference data including start/end times and associated spaces.
- **Participant Tracking**: View participant details for any conference including join/leave times and user types.
- **Access Controls**: Configure who can join meetings (open, trusted, or restricted) and which entry points are allowed.
- **Active Conference Management**: Programmatically end active conferences in meeting spaces.
In Sim, the Google Meet integration allows your agents to create meeting spaces on demand, monitor conference activity, track participation across meetings, and manage active conferences as part of automated workflows. This enables scenarios such as automatically provisioning meeting rooms for scheduled events, generating attendance reports, ending stale conferences, and building meeting analytics dashboards.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate Google Meet into your workflow. Create meeting spaces, get space details, end conferences, list conference records, and view participants.
## Tools
### `google_meet_create_space`
Create a new Google Meet meeting space
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessType` | string | No | Who can join the meeting without knocking: OPEN \(anyone with link\), TRUSTED \(org members\), RESTRICTED \(only invited\) |
| `entryPointAccess` | string | No | Entry points allowed: ALL \(all entry points\) or CREATOR_APP_ONLY \(only via app\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `name` | string | Resource name of the space \(e.g., spaces/abc123\) |
| `meetingUri` | string | Meeting URL \(e.g., https://meet.google.com/abc-defg-hij\) |
| `meetingCode` | string | Meeting code \(e.g., abc-defg-hij\) |
| `accessType` | string | Access type configuration |
| `entryPointAccess` | string | Entry point access configuration |
### `google_meet_get_space`
Get details of a Google Meet meeting space by name or meeting code
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `spaceName` | string | Yes | Space resource name \(spaces/abc123\) or meeting code \(abc-defg-hij\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `name` | string | Resource name of the space |
| `meetingUri` | string | Meeting URL |
| `meetingCode` | string | Meeting code |
| `accessType` | string | Access type configuration |
| `entryPointAccess` | string | Entry point access configuration |
| `activeConference` | string | Active conference record name |
### `google_meet_end_conference`
End the active conference in a Google Meet space
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `spaceName` | string | Yes | Space resource name \(e.g., spaces/abc123\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ended` | boolean | Whether the conference was ended successfully |
### `google_meet_list_conference_records`
List conference records for meetings you organized
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `filter` | string | No | Filter by space name \(e.g., space.name = "spaces/abc123"\) or time range \(e.g., start_time &gt; "2024-01-01T00:00:00Z"\) |
| `pageSize` | number | No | Maximum number of conference records to return \(max 100\) |
| `pageToken` | string | No | Page token from a previous list request |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `conferenceRecords` | json | List of conference records with name, start/end times, and space |
| `nextPageToken` | string | Token for next page of results |
### `google_meet_get_conference_record`
Get details of a specific conference record
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `conferenceName` | string | Yes | Conference record resource name \(e.g., conferenceRecords/abc123\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `name` | string | Conference record resource name |
| `startTime` | string | Conference start time |
| `endTime` | string | Conference end time |
| `expireTime` | string | Conference record expiration time |
| `space` | string | Associated space resource name |
### `google_meet_list_participants`
List participants of a conference record
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `conferenceName` | string | Yes | Conference record resource name \(e.g., conferenceRecords/abc123\) |
| `filter` | string | No | Filter participants \(e.g., earliest_start_time &gt; "2024-01-01T00:00:00Z"\) |
| `pageSize` | number | No | Maximum number of participants to return \(default 100, max 250\) |
| `pageToken` | string | No | Page token from a previous list request |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `participants` | json | List of participants with name, times, display name, and user type |
| `nextPageToken` | string | Token for next page of results |
| `totalSize` | number | Total number of participants |

View File

@@ -1014,4 +1014,36 @@ Get Jira users. If an account ID is provided, returns a single user. Otherwise,
| `startAt` | number | Pagination start index |
| `maxResults` | number | Maximum results per page |
### `jira_search_users`
Search for Jira users by email address or display name. Returns matching users with their accountId, displayName, and emailAddress.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `query` | string | Yes | A query string to search for users. Can be an email address, display name, or partial match. |
| `maxResults` | number | No | Maximum number of users to return \(default: 50, max: 1000\) |
| `startAt` | number | No | The index of the first user to return \(for pagination, default: 0\) |
| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | ISO 8601 timestamp of the operation |
| `users` | array | Array of matching Jira users |
| ↳ `accountId` | string | Atlassian account ID of the user |
| ↳ `displayName` | string | Display name of the user |
| ↳ `active` | boolean | Whether the user account is active |
| ↳ `emailAddress` | string | Email address of the user |
| ↳ `accountType` | string | Type of account \(e.g., atlassian, app, customer\) |
| ↳ `avatarUrl` | string | URL to the user avatar \(48x48\) |
| ↳ `timeZone` | string | User timezone |
| ↳ `self` | string | REST API URL for this user |
| `total` | number | Number of users returned in this page \(may be less than total matches\) |
| `startAt` | number | Pagination start index |
| `maxResults` | number | Maximum results per page |

View File

@@ -25,7 +25,7 @@ In Sim, the Knowledge Base block enables your agents to perform intelligent sema
## Usage Instructions
Integrate Knowledge into the workflow. Can search, upload chunks, and create documents.
Integrate Knowledge into the workflow. Perform full CRUD operations on documents, chunks, and tags.
@@ -122,4 +122,281 @@ Create a new document in a knowledge base
| `message` | string | Success or error message describing the operation result |
| `documentId` | string | ID of the created document |
### `knowledge_list_tags`
List all tag definitions for a knowledge base
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `knowledgeBaseId` | string | Yes | ID of the knowledge base to list tags for |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `knowledgeBaseId` | string | ID of the knowledge base |
| `tags` | array | Array of tag definitions for the knowledge base |
| ↳ `id` | string | Tag definition ID |
| ↳ `tagSlot` | string | Internal tag slot \(e.g. tag1, number1\) |
| ↳ `displayName` | string | Human-readable tag name |
| ↳ `fieldType` | string | Tag field type \(text, number, date, boolean\) |
| ↳ `createdAt` | string | Creation timestamp |
| ↳ `updatedAt` | string | Last update timestamp |
| `totalTags` | number | Total number of tag definitions |
### `knowledge_list_documents`
List documents in a knowledge base with optional filtering, search, and pagination
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `knowledgeBaseId` | string | Yes | ID of the knowledge base to list documents from |
| `search` | string | No | Search query to filter documents by filename |
| `enabledFilter` | string | No | Filter by enabled status: "all", "enabled", or "disabled" |
| `limit` | number | No | Maximum number of documents to return \(default: 50\) |
| `offset` | number | No | Number of documents to skip for pagination \(default: 0\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `knowledgeBaseId` | string | ID of the knowledge base |
| `documents` | array | Array of documents in the knowledge base |
| ↳ `id` | string | Document ID |
| ↳ `filename` | string | Document filename |
| ↳ `fileSize` | number | File size in bytes |
| ↳ `mimeType` | string | MIME type of the document |
| ↳ `enabled` | boolean | Whether the document is enabled |
| ↳ `processingStatus` | string | Processing status \(pending, processing, completed, failed\) |
| ↳ `chunkCount` | number | Number of chunks in the document |
| ↳ `tokenCount` | number | Total token count across chunks |
| ↳ `uploadedAt` | string | Upload timestamp |
| ↳ `updatedAt` | string | Last update timestamp |
| ↳ `connectorId` | string | Connector ID if document was synced from an external source |
| ↳ `connectorType` | string | Connector type \(e.g. notion, github, confluence\) if synced |
| ↳ `sourceUrl` | string | Original URL in the source system if synced from a connector |
| `totalDocuments` | number | Total number of documents matching the filter |
| `limit` | number | Page size used |
| `offset` | number | Offset used for pagination |
### `knowledge_get_document`
Get full details of a single document including tags, connector metadata, and processing status
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `knowledgeBaseId` | string | Yes | ID of the knowledge base the document belongs to |
| `documentId` | string | Yes | ID of the document to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Document ID |
| `filename` | string | Document filename |
| `fileSize` | number | File size in bytes |
| `mimeType` | string | MIME type of the document |
| `enabled` | boolean | Whether the document is enabled |
| `processingStatus` | string | Processing status \(pending, processing, completed, failed\) |
| `processingError` | string | Error message if processing failed |
| `chunkCount` | number | Number of chunks in the document |
| `tokenCount` | number | Total token count across chunks |
| `characterCount` | number | Total character count |
| `uploadedAt` | string | Upload timestamp |
| `updatedAt` | string | Last update timestamp |
| `connectorId` | string | Connector ID if document was synced from an external source |
| `sourceUrl` | string | Original URL in the source system if synced from a connector |
| `externalId` | string | External ID from the source system |
| `tags` | object | Tag values keyed by tag slot \(tag1-7, number1-5, date1-2, boolean1-3\) |
### `knowledge_delete_document`
Delete a document from a knowledge base
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `knowledgeBaseId` | string | Yes | ID of the knowledge base containing the document |
| `documentId` | string | Yes | ID of the document to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `documentId` | string | ID of the deleted document |
| `message` | string | Confirmation message |
### `knowledge_list_chunks`
List chunks for a document in a knowledge base with optional filtering and pagination
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `knowledgeBaseId` | string | Yes | ID of the knowledge base |
| `documentId` | string | Yes | ID of the document to list chunks from |
| `search` | string | No | Search query to filter chunks by content |
| `enabled` | string | No | Filter by enabled status: "true", "false", or "all" \(default: "all"\) |
| `limit` | number | No | Maximum number of chunks to return \(1-100, default: 50\) |
| `offset` | number | No | Number of chunks to skip for pagination \(default: 0\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `knowledgeBaseId` | string | ID of the knowledge base |
| `documentId` | string | ID of the document |
| `chunks` | array | Array of chunks in the document |
| ↳ `id` | string | Chunk ID |
| ↳ `chunkIndex` | number | Index of the chunk within the document |
| ↳ `content` | string | Chunk text content |
| ↳ `contentLength` | number | Content length in characters |
| ↳ `tokenCount` | number | Token count for the chunk |
| ↳ `enabled` | boolean | Whether the chunk is enabled |
| ↳ `createdAt` | string | Creation timestamp |
| ↳ `updatedAt` | string | Last update timestamp |
| `totalChunks` | number | Total number of chunks matching the filter |
| `limit` | number | Page size used |
| `offset` | number | Offset used for pagination |
### `knowledge_update_chunk`
Update the content or enabled status of a chunk in a knowledge base
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `knowledgeBaseId` | string | Yes | ID of the knowledge base |
| `documentId` | string | Yes | ID of the document containing the chunk |
| `chunkId` | string | Yes | ID of the chunk to update |
| `content` | string | No | New content for the chunk |
| `enabled` | boolean | No | Whether the chunk should be enabled or disabled |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `documentId` | string | ID of the parent document |
| `id` | string | Chunk ID |
| `chunkIndex` | number | Index of the chunk within the document |
| `content` | string | Updated chunk content |
| `contentLength` | number | Content length in characters |
| `tokenCount` | number | Token count for the chunk |
| `enabled` | boolean | Whether the chunk is enabled |
| `updatedAt` | string | Last update timestamp |
### `knowledge_delete_chunk`
Delete a chunk from a document in a knowledge base
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `knowledgeBaseId` | string | Yes | ID of the knowledge base |
| `documentId` | string | Yes | ID of the document containing the chunk |
| `chunkId` | string | Yes | ID of the chunk to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `chunkId` | string | ID of the deleted chunk |
| `documentId` | string | ID of the parent document |
| `message` | string | Confirmation message |
### `knowledge_list_connectors`
List all connectors for a knowledge base, showing sync status, type, and document counts
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `knowledgeBaseId` | string | Yes | ID of the knowledge base to list connectors for |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `knowledgeBaseId` | string | ID of the knowledge base |
| `connectors` | array | Array of connectors for the knowledge base |
| ↳ `id` | string | Connector ID |
| ↳ `connectorType` | string | Type of connector \(e.g. notion, github, confluence\) |
| ↳ `status` | string | Connector status \(active, paused, syncing\) |
| ↳ `syncIntervalMinutes` | number | Sync interval in minutes \(0 = manual only\) |
| ↳ `lastSyncAt` | string | Timestamp of last sync |
| ↳ `lastSyncError` | string | Error from last sync if failed |
| ↳ `lastSyncDocCount` | number | Number of documents synced in last sync |
| ↳ `nextSyncAt` | string | Timestamp of next scheduled sync |
| ↳ `consecutiveFailures` | number | Number of consecutive sync failures |
| ↳ `createdAt` | string | Creation timestamp |
| ↳ `updatedAt` | string | Last update timestamp |
| `totalConnectors` | number | Total number of connectors |
### `knowledge_get_connector`
Get detailed connector information including recent sync logs for monitoring sync health
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `knowledgeBaseId` | string | Yes | ID of the knowledge base the connector belongs to |
| `connectorId` | string | Yes | ID of the connector to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `connector` | object | Connector details |
| ↳ `id` | string | Connector ID |
| ↳ `connectorType` | string | Type of connector |
| ↳ `status` | string | Connector status \(active, paused, syncing\) |
| ↳ `syncIntervalMinutes` | number | Sync interval in minutes |
| ↳ `lastSyncAt` | string | Timestamp of last sync |
| ↳ `lastSyncError` | string | Error from last sync if failed |
| ↳ `lastSyncDocCount` | number | Docs synced in last sync |
| ↳ `nextSyncAt` | string | Next scheduled sync timestamp |
| ↳ `consecutiveFailures` | number | Consecutive sync failures |
| ↳ `createdAt` | string | Creation timestamp |
| ↳ `updatedAt` | string | Last update timestamp |
| `syncLogs` | array | Recent sync log entries |
| ↳ `id` | string | Sync log ID |
| ↳ `status` | string | Sync status |
| ↳ `startedAt` | string | Sync start time |
| ↳ `completedAt` | string | Sync completion time |
| ↳ `docsAdded` | number | Documents added |
| ↳ `docsUpdated` | number | Documents updated |
| ↳ `docsDeleted` | number | Documents deleted |
| ↳ `docsUnchanged` | number | Documents unchanged |
| ↳ `errorMessage` | string | Error message if sync failed |
### `knowledge_trigger_sync`
Trigger a manual sync for a knowledge base connector
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `knowledgeBaseId` | string | Yes | ID of the knowledge base the connector belongs to |
| `connectorId` | string | Yes | ID of the connector to trigger sync for |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `connectorId` | string | ID of the connector that was synced |
| `message` | string | Status message from the sync trigger |

View File

@@ -13,6 +13,7 @@
"asana",
"ashby",
"attio",
"brandfetch",
"browser_use",
"calcom",
"calendly",
@@ -28,11 +29,13 @@
"discord",
"dropbox",
"dspy",
"dub",
"duckduckgo",
"dynamodb",
"elasticsearch",
"elevenlabs",
"enrich",
"evernote",
"exa",
"file",
"firecrawl",
@@ -51,6 +54,7 @@
"google_forms",
"google_groups",
"google_maps",
"google_meet",
"google_pagespeed",
"google_search",
"google_sheets",
@@ -95,6 +99,7 @@
"mysql",
"neo4j",
"notion",
"obsidian",
"onedrive",
"onepassword",
"openai",

View File

@@ -0,0 +1,323 @@
---
title: Obsidian
description: Interact with your Obsidian vault via the Local REST API
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="obsidian"
color="#0F0F0F"
/>
## Usage Instructions
Read, create, update, search, and delete notes in your Obsidian vault. Manage periodic notes, execute commands, and patch content at specific locations. Requires the Obsidian Local REST API plugin.
## Tools
### `obsidian_append_active`
Append content to the currently active file in Obsidian
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `content` | string | Yes | Markdown content to append to the active file |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `appended` | boolean | Whether content was successfully appended |
### `obsidian_append_note`
Append content to an existing note in your Obsidian vault
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `filename` | string | Yes | Path to the note relative to vault root \(e.g. "folder/note.md"\) |
| `content` | string | Yes | Markdown content to append to the note |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `filename` | string | Path of the note |
| `appended` | boolean | Whether content was successfully appended |
### `obsidian_append_periodic_note`
Append content to the current periodic note (daily, weekly, monthly, quarterly, or yearly). Creates the note if it does not exist.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `period` | string | Yes | Period type: daily, weekly, monthly, quarterly, or yearly |
| `content` | string | Yes | Markdown content to append to the periodic note |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `period` | string | Period type of the note |
| `appended` | boolean | Whether content was successfully appended |
### `obsidian_create_note`
Create or replace a note in your Obsidian vault
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `filename` | string | Yes | Path for the note relative to vault root \(e.g. "folder/note.md"\) |
| `content` | string | Yes | Markdown content for the note |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `filename` | string | Path of the created note |
| `created` | boolean | Whether the note was successfully created |
### `obsidian_delete_note`
Delete a note from your Obsidian vault
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `filename` | string | Yes | Path to the note to delete relative to vault root |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `filename` | string | Path of the deleted note |
| `deleted` | boolean | Whether the note was successfully deleted |
### `obsidian_execute_command`
Execute a command in Obsidian (e.g. open daily note, toggle sidebar)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `commandId` | string | Yes | ID of the command to execute \(use List Commands operation to discover available commands\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `commandId` | string | ID of the executed command |
| `executed` | boolean | Whether the command was successfully executed |
### `obsidian_get_active`
Retrieve the content of the currently active file in Obsidian
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Markdown content of the active file |
| `filename` | string | Path to the active file |
### `obsidian_get_note`
Retrieve the content of a note from your Obsidian vault
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `filename` | string | Yes | Path to the note relative to vault root \(e.g. "folder/note.md"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Markdown content of the note |
| `filename` | string | Path to the note |
### `obsidian_get_periodic_note`
Retrieve the current periodic note (daily, weekly, monthly, quarterly, or yearly)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `period` | string | Yes | Period type: daily, weekly, monthly, quarterly, or yearly |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Markdown content of the periodic note |
| `period` | string | Period type of the note |
### `obsidian_list_commands`
List all available commands in Obsidian
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `commands` | json | List of available commands with IDs and names |
| ↳ `id` | string | Command identifier |
| ↳ `name` | string | Human-readable command name |
### `obsidian_list_files`
List files and directories in your Obsidian vault
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `path` | string | No | Directory path relative to vault root. Leave empty to list root. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `files` | json | List of files and directories |
| ↳ `path` | string | File or directory path |
| ↳ `type` | string | Whether the entry is a file or directory |
### `obsidian_open_file`
Open a file in the Obsidian UI (creates the file if it does not exist)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `filename` | string | Yes | Path to the file relative to vault root |
| `newLeaf` | boolean | No | Whether to open the file in a new leaf/tab |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `filename` | string | Path of the opened file |
| `opened` | boolean | Whether the file was successfully opened |
### `obsidian_patch_active`
Insert or replace content at a specific heading, block reference, or frontmatter field in the active file
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `content` | string | Yes | Content to insert at the target location |
| `operation` | string | Yes | How to insert content: append, prepend, or replace |
| `targetType` | string | Yes | Type of target: heading, block, or frontmatter |
| `target` | string | Yes | Target identifier \(heading text, block reference ID, or frontmatter field name\) |
| `targetDelimiter` | string | No | Delimiter for nested headings \(default: "::"\) |
| `trimTargetWhitespace` | boolean | No | Whether to trim whitespace from target before matching \(default: false\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `patched` | boolean | Whether the active file was successfully patched |
### `obsidian_patch_note`
Insert or replace content at a specific heading, block reference, or frontmatter field in a note
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `filename` | string | Yes | Path to the note relative to vault root \(e.g. "folder/note.md"\) |
| `content` | string | Yes | Content to insert at the target location |
| `operation` | string | Yes | How to insert content: append, prepend, or replace |
| `targetType` | string | Yes | Type of target: heading, block, or frontmatter |
| `target` | string | Yes | Target identifier \(heading text, block reference ID, or frontmatter field name\) |
| `targetDelimiter` | string | No | Delimiter for nested headings \(default: "::"\) |
| `trimTargetWhitespace` | boolean | No | Whether to trim whitespace from target before matching \(default: false\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `filename` | string | Path of the patched note |
| `patched` | boolean | Whether the note was successfully patched |
### `obsidian_search`
Search for text across notes in your Obsidian vault
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `query` | string | Yes | Text to search for across vault notes |
| `contextLength` | number | No | Number of characters of context around each match \(default: 100\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Search results with filenames, scores, and matching contexts |
| ↳ `filename` | string | Path to the matching note |
| ↳ `score` | number | Relevance score |
| ↳ `matches` | json | Matching text contexts |
| ↳ `context` | string | Text surrounding the match |

View File

@@ -24,7 +24,7 @@ These operations let your agents access and analyze Reddit content as part of yo
## Usage Instructions
Integrate Reddit into workflows. Read posts, comments, and search content. Submit posts, vote, reply, edit, and manage your Reddit account.
Integrate Reddit into workflows. Read posts, comments, and search content. Submit posts, vote, reply, edit, manage messages, and access user and subreddit info.
@@ -39,14 +39,15 @@ Fetch posts from a subreddit with different sorting options
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `subreddit` | string | Yes | The subreddit to fetch posts from \(e.g., "technology", "news"\) |
| `sort` | string | No | Sort method for posts \(e.g., "hot", "new", "top", "rising"\). Default: "hot" |
| `sort` | string | No | Sort method for posts \(e.g., "hot", "new", "top", "rising", "controversial"\). Default: "hot" |
| `limit` | number | No | Maximum number of posts to return \(e.g., 25\). Default: 10, max: 100 |
| `time` | string | No | Time filter for "top" sorted posts: "day", "week", "month", "year", or "all" \(default: "day"\) |
| `time` | string | No | Time filter for "top" sorted posts: "day", "week", "month", "year", or "all" \(default: "all"\) |
| `after` | string | No | Fullname of a thing to fetch items after \(for pagination\) |
| `before` | string | No | Fullname of a thing to fetch items before \(for pagination\) |
| `count` | number | No | A count of items already seen in the listing \(used for numbering\) |
| `show` | string | No | Show items that would normally be filtered \(e.g., "all"\) |
| `sr_detail` | boolean | No | Expand subreddit details in the response |
| `g` | string | No | Geo filter for posts \(e.g., "GLOBAL", "US", "AR", etc.\) |
#### Output
@@ -55,6 +56,7 @@ Fetch posts from a subreddit with different sorting options
| `subreddit` | string | Name of the subreddit where posts were fetched from |
| `posts` | array | Array of posts with title, author, URL, score, comments count, and metadata |
| ↳ `id` | string | Post ID |
| ↳ `name` | string | Thing fullname \(t3_xxxxx\) |
| ↳ `title` | string | Post title |
| ↳ `author` | string | Author username |
| ↳ `url` | string | Post URL |
@@ -66,6 +68,8 @@ Fetch posts from a subreddit with different sorting options
| ↳ `selftext` | string | Text content for self posts |
| ↳ `thumbnail` | string | Thumbnail URL |
| ↳ `subreddit` | string | Subreddit name |
| `after` | string | Fullname of the last item for forward pagination |
| `before` | string | Fullname of the first item for backward pagination |
### `reddit_get_comments`
@@ -83,12 +87,9 @@ Fetch comments from a specific Reddit post
| `context` | number | No | Number of parent comments to include |
| `showedits` | boolean | No | Show edit information for comments |
| `showmore` | boolean | No | Include "load more comments" elements in the response |
| `showtitle` | boolean | No | Include submission title in the response |
| `threaded` | boolean | No | Return comments in threaded/nested format |
| `truncate` | number | No | Integer to truncate comment depth |
| `after` | string | No | Fullname of a thing to fetch items after \(for pagination\) |
| `before` | string | No | Fullname of a thing to fetch items before \(for pagination\) |
| `count` | number | No | A count of items already seen in the listing \(used for numbering\) |
| `comment` | string | No | ID36 of a comment to focus on \(returns that comment thread\) |
#### Output
@@ -96,6 +97,7 @@ Fetch comments from a specific Reddit post
| --------- | ---- | ----------- |
| `post` | object | Post information including ID, title, author, content, and metadata |
| ↳ `id` | string | Post ID |
| ↳ `name` | string | Thing fullname \(t3_xxxxx\) |
| ↳ `title` | string | Post title |
| ↳ `author` | string | Post author |
| ↳ `selftext` | string | Post text content |
@@ -104,6 +106,7 @@ Fetch comments from a specific Reddit post
| ↳ `permalink` | string | Reddit permalink |
| `comments` | array | Nested comments with author, body, score, timestamps, and replies |
| ↳ `id` | string | Comment ID |
| ↳ `name` | string | Thing fullname \(t1_xxxxx\) |
| ↳ `author` | string | Comment author |
| ↳ `body` | string | Comment text |
| ↳ `score` | number | Comment score |
@@ -135,6 +138,7 @@ Fetch controversial posts from a subreddit
| `subreddit` | string | Name of the subreddit where posts were fetched from |
| `posts` | array | Array of controversial posts with title, author, URL, score, comments count, and metadata |
| ↳ `id` | string | Post ID |
| ↳ `name` | string | Thing fullname \(t3_xxxxx\) |
| ↳ `title` | string | Post title |
| ↳ `author` | string | Author username |
| ↳ `url` | string | Post URL |
@@ -146,6 +150,8 @@ Fetch controversial posts from a subreddit
| ↳ `selftext` | string | Text content for self posts |
| ↳ `thumbnail` | string | Thumbnail URL |
| ↳ `subreddit` | string | Subreddit name |
| `after` | string | Fullname of the last item for forward pagination |
| `before` | string | Fullname of the first item for backward pagination |
### `reddit_search`
@@ -165,6 +171,8 @@ Search for posts within a subreddit
| `before` | string | No | Fullname of a thing to fetch items before \(for pagination\) |
| `count` | number | No | A count of items already seen in the listing \(used for numbering\) |
| `show` | string | No | Show items that would normally be filtered \(e.g., "all"\) |
| `type` | string | No | Type of search results: "link" \(posts\), "sr" \(subreddits\), or "user" \(users\). Default: "link" |
| `sr_detail` | boolean | No | Expand subreddit details in the response |
#### Output
@@ -173,6 +181,7 @@ Search for posts within a subreddit
| `subreddit` | string | Name of the subreddit where search was performed |
| `posts` | array | Array of search result posts with title, author, URL, score, comments count, and metadata |
| ↳ `id` | string | Post ID |
| ↳ `name` | string | Thing fullname \(t3_xxxxx\) |
| ↳ `title` | string | Post title |
| ↳ `author` | string | Author username |
| ↳ `url` | string | Post URL |
@@ -184,6 +193,8 @@ Search for posts within a subreddit
| ↳ `selftext` | string | Text content for self posts |
| ↳ `thumbnail` | string | Thumbnail URL |
| ↳ `subreddit` | string | Subreddit name |
| `after` | string | Fullname of the last item for forward pagination |
| `before` | string | Fullname of the first item for backward pagination |
### `reddit_submit_post`
@@ -200,6 +211,9 @@ Submit a new post to a subreddit (text or link)
| `nsfw` | boolean | No | Mark post as NSFW |
| `spoiler` | boolean | No | Mark post as spoiler |
| `send_replies` | boolean | No | Send reply notifications to inbox \(default: true\) |
| `flair_id` | string | No | Flair template UUID for the post \(max 36 characters\) |
| `flair_text` | string | No | Flair text to display on the post \(max 64 characters\) |
| `collection_id` | string | No | Collection UUID to add the post to |
#### Output
@@ -264,6 +278,21 @@ Save a Reddit post or comment to your saved items
| `posts` | json | Posts data |
| `post` | json | Single post data |
| `comments` | json | Comments data |
| `success` | boolean | Operation success status |
| `message` | string | Result message |
| `data` | json | Response data |
| `after` | string | Pagination cursor \(next page\) |
| `before` | string | Pagination cursor \(previous page\) |
| `id` | string | Entity ID |
| `name` | string | Entity fullname |
| `messages` | json | Messages data |
| `display_name` | string | Subreddit display name |
| `subscribers` | number | Subscriber count |
| `description` | string | Description text |
| `link_karma` | number | Link karma |
| `comment_karma` | number | Comment karma |
| `total_karma` | number | Total karma |
| `icon_img` | string | Icon image URL |
### `reddit_reply`
@@ -275,6 +304,7 @@ Add a comment reply to a Reddit post or comment
| --------- | ---- | -------- | ----------- |
| `parent_id` | string | Yes | Thing fullname to reply to \(e.g., "t3_abc123" for post, "t1_def456" for comment\) |
| `text` | string | Yes | Comment text in markdown format \(e.g., "Great post! Here is my **reply**"\) |
| `return_rtjson` | boolean | No | Return response in Rich Text JSON format |
#### Output
@@ -345,4 +375,138 @@ Subscribe or unsubscribe from a subreddit
| `success` | boolean | Whether the subscription action was successful |
| `message` | string | Success or error message |
### `reddit_get_me`
Get information about the authenticated Reddit user
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | User ID |
| `name` | string | Username |
| `created_utc` | number | Account creation time in UTC epoch seconds |
| `link_karma` | number | Total link karma |
| `comment_karma` | number | Total comment karma |
| `total_karma` | number | Combined total karma |
| `is_gold` | boolean | Whether user has Reddit Premium |
| `is_mod` | boolean | Whether user is a moderator |
| `has_verified_email` | boolean | Whether email is verified |
| `icon_img` | string | User avatar/icon URL |
### `reddit_get_user`
Get public profile information about any Reddit user by username
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `username` | string | Yes | Reddit username to look up \(e.g., "spez", "example_user"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | User ID |
| `name` | string | Username |
| `created_utc` | number | Account creation time in UTC epoch seconds |
| `link_karma` | number | Total link karma |
| `comment_karma` | number | Total comment karma |
| `total_karma` | number | Combined total karma |
| `is_gold` | boolean | Whether user has Reddit Premium |
| `is_mod` | boolean | Whether user is a moderator |
| `has_verified_email` | boolean | Whether email is verified |
| `icon_img` | string | User avatar/icon URL |
### `reddit_send_message`
Send a private message to a Reddit user
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `to` | string | Yes | Recipient username \(e.g., "example_user"\) or subreddit \(e.g., "/r/subreddit"\) |
| `subject` | string | Yes | Message subject \(max 100 characters\) |
| `text` | string | Yes | Message body in markdown format |
| `from_sr` | string | No | Subreddit name to send the message from \(requires moderator mail permission\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the message was sent successfully |
| `message` | string | Success or error message |
### `reddit_get_messages`
Retrieve private messages from your Reddit inbox
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `where` | string | No | Message folder to retrieve: "inbox" \(all\), "unread", "sent", "messages" \(direct messages only\), "comments" \(comment replies\), "selfreply" \(self-post replies\), or "mentions" \(username mentions\). Default: "inbox" |
| `limit` | number | No | Maximum number of messages to return \(e.g., 25\). Default: 25, max: 100 |
| `after` | string | No | Fullname of a thing to fetch items after \(for pagination\) |
| `before` | string | No | Fullname of a thing to fetch items before \(for pagination\) |
| `mark` | boolean | No | Whether to mark fetched messages as read |
| `count` | number | No | A count of items already seen in the listing \(used for numbering\) |
| `show` | string | No | Show items that would normally be filtered \(e.g., "all"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `messages` | array | Array of messages with sender, recipient, subject, body, and metadata |
| ↳ `id` | string | Message ID |
| ↳ `name` | string | Thing fullname \(t4_xxxxx\) |
| ↳ `author` | string | Sender username |
| ↳ `dest` | string | Recipient username |
| ↳ `subject` | string | Message subject |
| ↳ `body` | string | Message body text |
| ↳ `created_utc` | number | Creation time in UTC epoch seconds |
| ↳ `new` | boolean | Whether the message is unread |
| ↳ `was_comment` | boolean | Whether the message is a comment reply |
| ↳ `context` | string | Context URL for comment replies |
| ↳ `distinguished` | string | Distinction: null/"moderator"/"admin" |
| `after` | string | Fullname of the last item for forward pagination |
| `before` | string | Fullname of the first item for backward pagination |
### `reddit_get_subreddit_info`
Get metadata and information about a subreddit
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `subreddit` | string | Yes | The subreddit to get info about \(e.g., "technology", "programming", "news"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Subreddit ID |
| `name` | string | Subreddit fullname \(t5_xxxxx\) |
| `display_name` | string | Subreddit name without prefix |
| `title` | string | Subreddit title |
| `description` | string | Full subreddit description \(markdown\) |
| `public_description` | string | Short public description |
| `subscribers` | number | Number of subscribers |
| `accounts_active` | number | Number of currently active users |
| `created_utc` | number | Creation time in UTC epoch seconds |
| `over18` | boolean | Whether the subreddit is NSFW |
| `lang` | string | Primary language of the subreddit |
| `subreddit_type` | string | Subreddit type: public, private, restricted, etc. |
| `url` | string | Subreddit URL path \(e.g., /r/technology/\) |
| `icon_img` | string | Subreddit icon URL |
| `banner_img` | string | Subreddit banner URL |

View File

@@ -69,7 +69,9 @@ Read records from a ServiceNow table
| `number` | string | No | Record number \(e.g., INC0010001\) |
| `query` | string | No | Encoded query string \(e.g., "active=true^priority=1"\) |
| `limit` | number | No | Maximum number of records to return \(e.g., 10, 50, 100\) |
| `offset` | number | No | Number of records to skip for pagination \(e.g., 0, 10, 20\) |
| `fields` | string | No | Comma-separated list of fields to return \(e.g., sys_id,number,short_description,state\) |
| `displayValue` | string | No | Return display values for reference fields: "true" \(display only\), "false" \(sys_id only\), or "all" \(both\) |
#### Output

View File

@@ -1,6 +1,6 @@
---
title: Slack
description: Send, update, delete messages, send ephemeral messages, add reactions in Slack or trigger workflows from Slack events
description: Send, update, delete messages, manage views and modals, add or remove reactions, manage canvases, get channel info and user presence in Slack
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
@@ -39,7 +39,7 @@ If you encounter issues with the Slack integration, contact us at [help@sim.ai](
## Usage Instructions
Integrate Slack into the workflow. Can send, update, and delete messages, send ephemeral messages visible only to a specific user, create canvases, read messages, and add reactions. Requires Bot Token instead of OAuth in advanced mode. Can be used in trigger mode to trigger a workflow when a message is sent to a channel.
Integrate Slack into the workflow. Can send, update, and delete messages, send ephemeral messages visible only to a specific user, open/update/push modal views, publish Home tab views, create canvases, read messages, and add or remove reactions. Requires Bot Token instead of OAuth in advanced mode. Can be used in trigger mode to trigger a workflow when a message is sent to a channel.
@@ -799,4 +799,313 @@ Add an emoji reaction to a Slack message
| ↳ `timestamp` | string | Message timestamp |
| ↳ `reaction` | string | Emoji reaction name |
### `slack_remove_reaction`
Remove an emoji reaction from a Slack message
#### 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 | Channel ID where the message was posted \(e.g., C1234567890\) |
| `timestamp` | string | Yes | Timestamp of the message to remove reaction from \(e.g., 1405894322.002768\) |
| `name` | string | Yes | Name of the emoji reaction to remove \(without colons, e.g., thumbsup, heart, eyes\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Success message |
| `metadata` | object | Reaction metadata |
| ↳ `channel` | string | Channel ID |
| ↳ `timestamp` | string | Message timestamp |
| ↳ `reaction` | string | Emoji reaction name |
### `slack_get_channel_info`
Get detailed information about a Slack channel by its ID
#### 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 | Channel ID to get information about \(e.g., C1234567890\) |
| `includeNumMembers` | boolean | No | Whether to include the member count in the response |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `channelInfo` | object | Detailed channel information |
| ↳ `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_get_user_presence`
Check whether a Slack user is currently active or away
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | No | Authentication method: oauth or bot_token |
| `botToken` | string | No | Bot token for Custom Bot |
| `userId` | string | Yes | User ID to check presence for \(e.g., U1234567890\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `presence` | string | User presence status: "active" or "away" |
| `online` | boolean | Whether user has an active client connection \(only available when checking own presence\) |
| `autoAway` | boolean | Whether user was automatically set to away due to inactivity \(only available when checking own presence\) |
| `manualAway` | boolean | Whether user manually set themselves as away \(only available when checking own presence\) |
| `connectionCount` | number | Total number of active connections for the user \(only available when checking own presence\) |
| `lastActivity` | number | Unix timestamp of last detected activity \(only available when checking own presence\) |
### `slack_edit_canvas`
Edit an existing Slack canvas by inserting, replacing, or deleting content
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | No | Authentication method: oauth or bot_token |
| `botToken` | string | No | Bot token for Custom Bot |
| `canvasId` | string | Yes | Canvas ID to edit \(e.g., F1234ABCD\) |
| `operation` | string | Yes | Edit operation: insert_at_start, insert_at_end, insert_after, insert_before, replace, delete, or rename |
| `content` | string | No | Markdown content for the operation \(required for insert/replace operations\) |
| `sectionId` | string | No | Section ID to target \(required for insert_after, insert_before, replace, and delete\) |
| `title` | string | No | New title for the canvas \(only used with rename operation\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Success message |
### `slack_create_channel_canvas`
Create a canvas pinned to a Slack channel as its resource hub
#### 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 | Channel ID to create the canvas in \(e.g., C1234567890\) |
| `title` | string | No | Title for the channel canvas |
| `content` | string | No | Canvas content in markdown format |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `canvas_id` | string | ID of the created channel canvas |
### `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.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | No | Authentication method: oauth or bot_token |
| `botToken` | string | No | Bot token for Custom Bot |
| `triggerId` | string | Yes | Exchange a trigger to post to the user. Obtained from an interaction payload \(e.g., slash command, button click\) |
| `interactivityPointer` | string | No | Alternative to trigger_id for posting to user |
| `view` | json | Yes | A view payload object defining the modal. Must include type \("modal"\), title, and blocks array |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `view` | object | The opened modal view object |
| ↳ `id` | string | Unique view identifier |
| ↳ `team_id` | string | Workspace/team ID |
| ↳ `type` | string | View type \(e.g., "modal"\) |
| ↳ `title` | json | Plain text title object with type and text fields |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Title text content |
| ↳ `submit` | json | Plain text submit button object |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Submit button text |
| ↳ `close` | json | Plain text close button object |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Close button text |
| ↳ `blocks` | array | Block Kit blocks in the view |
| ↳ `type` | string | Block type \(section, divider, image, actions, etc.\) |
| ↳ `block_id` | string | Unique block identifier |
| ↳ `private_metadata` | string | Private metadata string passed with the view |
| ↳ `callback_id` | string | Custom identifier for the view |
| ↳ `external_id` | string | Custom external identifier \(max 255 chars, unique per workspace\) |
| ↳ `state` | json | Current state of the view with input values |
| ↳ `hash` | string | View version hash for updates |
| ↳ `clear_on_close` | boolean | Whether to clear all views in the stack when this view is closed |
| ↳ `notify_on_close` | boolean | Whether to send a view_closed event when this view is closed |
| ↳ `root_view_id` | string | ID of the root view in the view stack |
| ↳ `previous_view_id` | string | ID of the previous view in the view stack |
| ↳ `app_id` | string | Application identifier |
| ↳ `bot_id` | string | Bot identifier |
### `slack_update_view`
Update an existing modal view in Slack. Identify the view by view_id or external_id, and provide the updated view payload.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | No | Authentication method: oauth or bot_token |
| `botToken` | string | No | Bot token for Custom Bot |
| `viewId` | string | No | Unique identifier of the view to update. Either viewId or externalId is required |
| `externalId` | string | No | Developer-set unique identifier of the view to update \(max 255 chars\). Either viewId or externalId is required |
| `hash` | string | No | View state hash to protect against race conditions. Obtained from a previous views response |
| `view` | json | Yes | A view payload object defining the updated modal. Must include type \("modal"\), title, and blocks array. Use identical block_id and action_id values to preserve input data |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `view` | object | The updated modal view object |
| ↳ `id` | string | Unique view identifier |
| ↳ `team_id` | string | Workspace/team ID |
| ↳ `type` | string | View type \(e.g., "modal"\) |
| ↳ `title` | json | Plain text title object with type and text fields |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Title text content |
| ↳ `submit` | json | Plain text submit button object |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Submit button text |
| ↳ `close` | json | Plain text close button object |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Close button text |
| ↳ `blocks` | array | Block Kit blocks in the view |
| ↳ `type` | string | Block type \(section, divider, image, actions, etc.\) |
| ↳ `block_id` | string | Unique block identifier |
| ↳ `private_metadata` | string | Private metadata string passed with the view |
| ↳ `callback_id` | string | Custom identifier for the view |
| ↳ `external_id` | string | Custom external identifier \(max 255 chars, unique per workspace\) |
| ↳ `state` | json | Current state of the view with input values |
| ↳ `hash` | string | View version hash for updates |
| ↳ `clear_on_close` | boolean | Whether to clear all views in the stack when this view is closed |
| ↳ `notify_on_close` | boolean | Whether to send a view_closed event when this view is closed |
| ↳ `root_view_id` | string | ID of the root view in the view stack |
| ↳ `previous_view_id` | string | ID of the previous view in the view stack |
| ↳ `app_id` | string | Application identifier |
| ↳ `bot_id` | string | Bot identifier |
### `slack_push_view`
Push a new view onto an existing modal stack in Slack. Limited to 2 additional views after the initial modal is opened.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | No | Authentication method: oauth or bot_token |
| `botToken` | string | No | Bot token for Custom Bot |
| `triggerId` | string | Yes | Exchange a trigger to post to the user. Obtained from an interaction payload \(e.g., button click within an existing modal\) |
| `interactivityPointer` | string | No | Alternative to trigger_id for posting to user |
| `view` | json | Yes | A view payload object defining the modal to push. Must include type \("modal"\), title, and blocks array |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `view` | object | The pushed modal view object |
| ↳ `id` | string | Unique view identifier |
| ↳ `team_id` | string | Workspace/team ID |
| ↳ `type` | string | View type \(e.g., "modal"\) |
| ↳ `title` | json | Plain text title object with type and text fields |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Title text content |
| ↳ `submit` | json | Plain text submit button object |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Submit button text |
| ↳ `close` | json | Plain text close button object |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Close button text |
| ↳ `blocks` | array | Block Kit blocks in the view |
| ↳ `type` | string | Block type \(section, divider, image, actions, etc.\) |
| ↳ `block_id` | string | Unique block identifier |
| ↳ `private_metadata` | string | Private metadata string passed with the view |
| ↳ `callback_id` | string | Custom identifier for the view |
| ↳ `external_id` | string | Custom external identifier \(max 255 chars, unique per workspace\) |
| ↳ `state` | json | Current state of the view with input values |
| ↳ `hash` | string | View version hash for updates |
| ↳ `clear_on_close` | boolean | Whether to clear all views in the stack when this view is closed |
| ↳ `notify_on_close` | boolean | Whether to send a view_closed event when this view is closed |
| ↳ `root_view_id` | string | ID of the root view in the view stack |
| ↳ `previous_view_id` | string | ID of the previous view in the view stack |
| ↳ `app_id` | string | Application identifier |
| ↳ `bot_id` | string | Bot identifier |
### `slack_publish_view`
Publish a static view to a user
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | No | Authentication method: oauth or bot_token |
| `botToken` | string | No | Bot token for Custom Bot |
| `userId` | string | Yes | The user ID to publish the Home tab view to \(e.g., U0BPQUNTA\) |
| `hash` | string | No | View state hash to protect against race conditions. Obtained from a previous views response |
| `view` | json | Yes | A view payload object defining the Home tab. Must include type \("home"\) and blocks array |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `view` | object | The published Home tab view object |
| ↳ `id` | string | Unique view identifier |
| ↳ `team_id` | string | Workspace/team ID |
| ↳ `type` | string | View type \(e.g., "modal"\) |
| ↳ `title` | json | Plain text title object with type and text fields |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Title text content |
| ↳ `submit` | json | Plain text submit button object |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Submit button text |
| ↳ `close` | json | Plain text close button object |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Close button text |
| ↳ `blocks` | array | Block Kit blocks in the view |
| ↳ `type` | string | Block type \(section, divider, image, actions, etc.\) |
| ↳ `block_id` | string | Unique block identifier |
| ↳ `private_metadata` | string | Private metadata string passed with the view |
| ↳ `callback_id` | string | Custom identifier for the view |
| ↳ `external_id` | string | Custom external identifier \(max 255 chars, unique per workspace\) |
| ↳ `state` | json | Current state of the view with input values |
| ↳ `hash` | string | View version hash for updates |
| ↳ `clear_on_close` | boolean | Whether to clear all views in the stack when this view is closed |
| ↳ `notify_on_close` | boolean | Whether to send a view_closed event when this view is closed |
| ↳ `root_view_id` | string | ID of the root view in the view stack |
| ↳ `previous_view_id` | string | ID of the previous view in the view stack |
| ↳ `app_id` | string | Application identifier |
| ↳ `bot_id` | string | Bot identifier |

View File

@@ -183,13 +183,8 @@ while (count < items.length) {
### Limitaciones
<Callout type="warning">
Los bloques contenedores (Bucles y Paralelos) no pueden anidarse unos dentro de otros. Esto significa:
- No puedes colocar un bloque de Bucle dentro de otro bloque de Bucle
- No puedes colocar un bloque Paralelo dentro de un bloque de Bucle
- No puedes colocar ningún bloque contenedor dentro de otro bloque contenedor
Si necesitas iteración multidimensional, considera reestructurar tu flujo de trabajo para usar bucles secuenciales o procesar datos por etapas.
<Callout type="info">
Los bloques contenedores (Bucles y Paralelos) admiten anidamiento. Puedes colocar bucles dentro de bucles, paralelos dentro de bucles, y cualquier combinación de bloques contenedores para construir flujos de trabajo multidimensionales complejos.
</Callout>
<Callout type="info">

View File

@@ -117,11 +117,8 @@ Cada instancia paralela se ejecuta de forma independiente:
### Limitaciones
<Callout type="warning">
Los bloques contenedores (Bucles y Paralelos) no pueden anidarse unos dentro de otros. Esto significa:
- No puedes colocar un bloque de Bucle dentro de un bloque Paralelo
- No puedes colocar otro bloque Paralelo dentro de un bloque Paralelo
- No puedes colocar ningún bloque contenedor dentro de otro bloque contenedor
<Callout type="info">
Los bloques contenedores (Bucles y Paralelos) admiten anidamiento. Puedes colocar paralelos dentro de paralelos, bucles dentro de paralelos, y cualquier combinación de bloques contenedores para construir flujos de trabajo multidimensionales complejos.
</Callout>
<Callout type="info">

View File

@@ -190,13 +190,8 @@ return results;
### Limitations
<Callout type="warning">
Les blocs conteneurs (Boucles et Parallèles) ne peuvent pas être imbriqués les uns dans les autres. Cela signifie :
- Vous ne pouvez pas placer un bloc Boucle à l'intérieur d'un autre bloc Boucle
- Vous ne pouvez pas placer un bloc Parallèle à l'intérieur d'un bloc Boucle
- Vous ne pouvez pas placer un bloc conteneur à l'intérieur d'un autre bloc conteneur
Si vous avez besoin d'une itération multidimensionnelle, envisagez de restructurer votre flux de travail pour utiliser des boucles séquentielles ou traiter les données par étapes.
<Callout type="info">
Les blocs conteneurs (Boucles et Parallèles) prennent en charge l'imbrication. Vous pouvez placer des boucles dans des boucles, des parallèles dans des boucles, et toute combinaison de blocs conteneurs pour construire des flux de travail multidimensionnels complexes.
</Callout>
<Callout type="info">

View File

@@ -117,11 +117,8 @@ Chaque instance parallèle s'exécute indépendamment :
### Limitations
<Callout type="warning">
Les blocs conteneurs (Boucles et Parallèles) ne peuvent pas être imbriqués les uns dans les autres. Cela signifie :
- Vous ne pouvez pas placer un bloc de Boucle à l'intérieur d'un bloc Parallèle
- Vous ne pouvez pas placer un autre bloc Parallèle à l'intérieur d'un bloc Parallèle
- Vous ne pouvez pas placer un bloc conteneur à l'intérieur d'un autre bloc conteneur
<Callout type="info">
Les blocs conteneurs (Boucles et Parallèles) prennent en charge l'imbrication. Vous pouvez placer des parallèles dans des parallèles, des boucles dans des parallèles, et toute combinaison de blocs conteneurs pour construire des flux de travail multidimensionnels complexes.
</Callout>
<Callout type="info">

View File

@@ -198,13 +198,8 @@ while (counter < items.length && !foundTarget) {
### 制限事項
<Callout type="warning">
コンテナブロック(ループと並列)は互いに入れ子にすることができません。つまり:
- ループブロックを別のループブロック内に配置することはできません
- 並列ブロックをループブロック内に配置することはできません
- どのコンテナブロックも別のコンテナブロック内に配置することはできません
多次元反復が必要な場合は、順次ループを使用するか、データを段階的に処理するようにワークフローを再構成することを検討してください。
<Callout type="info">
コンテナブロック(ループと並列)はネストをサポートしています。ループの中にループを、ループの中に並列を、そして任意のコンテナブロックの組み合わせで複雑な多次元ワークフローを構築できます。
</Callout>
<Callout type="info">

View File

@@ -117,11 +117,8 @@ Parallel (["gpt-4o", "claude-3.7-sonnet", "gemini-2.5-pro"]) → Agent → Evalu
### 制限事項
<Callout type="warning">
コンテナブロック(ループと並列)は互いにネストできません。つまり:
- 並列ブロック内にループブロックを配置できません
- 並列ブロック内に別の並列ブロックを配置できません
- どのコンテナブロック内にも別のコンテナブロックを配置できません
<Callout type="info">
コンテナブロック(ループと並列)はネストをサポートしています。並列の中に並列を、並列の中にループを、そして任意のコンテナブロックの組み合わせで複雑な多次元ワークフローを構築できます。
</Callout>
<Callout type="info">

View File

@@ -160,13 +160,8 @@ Variables (i=0) → Loop (While i<10) → Agent (Process) → Variables (i++)
### 限制
<Callout type="warning">
容器块(循环和并行)不能嵌套在彼此内部。这意味着:
- 您不能将一个循环块放入另一个循环块中
- 您不能将一个并行块放入循环块中
- 您不能将任何容器块放入另一个容器块中
如果您需要多维迭代,请考虑重构您的工作流以使用顺序循环或分阶段处理数据。
<Callout type="info">
容器块(循环和并行)支持嵌套。您可以将循环放入循环中、将并行放入循环中,以及任意组合容器块来构建复杂的多维工作流。
</Callout>
<Callout type="info">

View File

@@ -121,11 +121,8 @@ const allResults = input.parallel.results;
### 限制
<Callout type="warning">
容器块(循环和并行)不能嵌套在彼此内部。这意味着:
- 您不能在并行块中放置循环块
- 您不能在并行块中放置另一个并行块
- 您不能在一个容器块中放置另一个容器块
<Callout type="info">
容器块(循环和并行)支持嵌套。您可以将并行放入并行中、将循环放入并行中,以及任意组合容器块来构建复杂的多维工作流。
</Callout>
<Callout type="info">

View File

@@ -62,7 +62,10 @@ function openapiPluginBadgeLeft() {
null,
createElement(
'span',
{ className: `font-mono font-medium me-1.5 text-[10px] text-nowrap ${colorClass}` },
{
className: `font-mono font-medium me-1.5 text-[10px] text-nowrap ${colorClass}`,
'data-method': method.toLowerCase(),
},
method
),
node.name

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,11 @@
"build": "fumadocs-mdx && NODE_OPTIONS='--max-old-space-size=8192' next build",
"start": "next start",
"postinstall": "fumadocs-mdx",
"type-check": "tsc --noEmit"
"type-check": "tsc --noEmit",
"lint": "biome check --write --unsafe .",
"lint:check": "biome check .",
"format": "biome format --write .",
"format:check": "biome format ."
},
"dependencies": {
"@sim/db": "workspace:*",

View File

@@ -0,0 +1,37 @@
'use client'
import { useEffect } from 'react'
import AuthBackground from '@/app/(auth)/components/auth-background'
import Nav from '@/app/(landing)/components/nav/nav'
function isColorDark(hexColor: string): boolean {
const hex = hexColor.replace('#', '')
const r = Number.parseInt(hex.substr(0, 2), 16)
const g = Number.parseInt(hex.substr(2, 2), 16)
const b = Number.parseInt(hex.substr(4, 2), 16)
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
return luminance < 0.5
}
export default function AuthLayoutClient({ children }: { children: React.ReactNode }) {
useEffect(() => {
const rootStyle = getComputedStyle(document.documentElement)
const brandBackground = rootStyle.getPropertyValue('--brand-background-hex').trim()
if (brandBackground && isColorDark(brandBackground)) {
document.body.classList.add('auth-dark-bg')
} else {
document.body.classList.remove('auth-dark-bg')
}
}, [])
return (
<AuthBackground>
<main className='relative flex min-h-screen flex-col text-foreground'>
<Nav hideAuthButtons={true} variant='auth' />
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>
<div className='w-full max-w-lg px-4'>{children}</div>
</div>
</main>
</AuthBackground>
)
}

View File

@@ -1,42 +1,10 @@
'use client'
import type { Metadata } from 'next'
import AuthLayoutClient from '@/app/(auth)/auth-layout-client'
import { useEffect } from 'react'
import AuthBackground from '@/app/(auth)/components/auth-background'
import Nav from '@/app/(landing)/components/nav/nav'
// Helper to detect if a color is dark
function isColorDark(hexColor: string): boolean {
const hex = hexColor.replace('#', '')
const r = Number.parseInt(hex.substr(0, 2), 16)
const g = Number.parseInt(hex.substr(2, 2), 16)
const b = Number.parseInt(hex.substr(4, 2), 16)
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
return luminance < 0.5
export const metadata: Metadata = {
robots: { index: false, follow: false },
}
export default function AuthLayout({ children }: { children: React.ReactNode }) {
useEffect(() => {
// Check if brand background is dark and add class accordingly
const rootStyle = getComputedStyle(document.documentElement)
const brandBackground = rootStyle.getPropertyValue('--brand-background-hex').trim()
if (brandBackground && isColorDark(brandBackground)) {
document.body.classList.add('auth-dark-bg')
} else {
document.body.classList.remove('auth-dark-bg')
}
}, [])
return (
<AuthBackground>
<main className='relative flex min-h-screen flex-col text-foreground'>
{/* Header - Nav handles all conditional logic */}
<Nav hideAuthButtons={true} variant='auth' />
{/* Content */}
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>
<div className='w-full max-w-lg px-4'>{children}</div>
</div>
</main>
</AuthBackground>
)
return <AuthLayoutClient>{children}</AuthLayoutClient>
}

View File

@@ -1,6 +1,11 @@
import type { Metadata } from 'next'
import { getOAuthProviderStatus } from '@/app/(auth)/components/oauth-provider-checker'
import LoginForm from '@/app/(auth)/login/login-form'
export const metadata: Metadata = {
title: 'Log In',
}
export const dynamic = 'force-dynamic'
export default async function LoginPage() {

View File

@@ -1,117 +1,8 @@
'use client'
import type { Metadata } from 'next'
import ResetPasswordPage from '@/app/(auth)/reset-password/reset-password-content'
import { Suspense, useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { SetNewPasswordForm } from '@/app/(auth)/reset-password/reset-password-form'
const logger = createLogger('ResetPasswordPage')
function ResetPasswordContent() {
const router = useRouter()
const searchParams = useSearchParams()
const token = searchParams.get('token')
const [isSubmitting, setIsSubmitting] = useState(false)
const [statusMessage, setStatusMessage] = useState<{
type: 'success' | 'error' | null
text: string
}>({
type: null,
text: '',
})
useEffect(() => {
if (!token) {
setStatusMessage({
type: 'error',
text: 'Invalid or missing reset token. Please request a new password reset link.',
})
}
}, [token])
const handleResetPassword = async (password: string) => {
try {
setIsSubmitting(true)
setStatusMessage({ type: null, text: '' })
const response = await fetch('/api/auth/reset-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
token,
newPassword: password,
}),
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.message || 'Failed to reset password')
}
setStatusMessage({
type: 'success',
text: 'Password reset successful! Redirecting to login...',
})
setTimeout(() => {
router.push('/login?resetSuccess=true')
}, 1500)
} catch (error) {
logger.error('Error resetting password:', { error })
setStatusMessage({
type: 'error',
text: error instanceof Error ? error.message : 'Failed to reset password',
})
} finally {
setIsSubmitting(false)
}
}
return (
<>
<div className='space-y-1 text-center'>
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
Reset your password
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
Enter a new password for your account
</p>
</div>
<div className={`${inter.className} mt-8`}>
<SetNewPasswordForm
token={token}
onSubmit={handleResetPassword}
isSubmitting={isSubmitting}
statusType={statusMessage.type}
statusMessage={statusMessage.text}
/>
</div>
<div className={`${inter.className} pt-6 text-center font-light text-[14px]`}>
<Link
href='/login'
className='font-medium text-[var(--brand-accent-hex)] underline-offset-4 transition hover:text-[var(--brand-accent-hover-hex)] hover:underline'
>
Back to login
</Link>
</div>
</>
)
export const metadata: Metadata = {
title: 'Reset Password',
}
export default function ResetPasswordPage() {
return (
<Suspense
fallback={<div className='flex h-screen items-center justify-center'>Loading...</div>}
>
<ResetPasswordContent />
</Suspense>
)
}
export default ResetPasswordPage

View File

@@ -0,0 +1,117 @@
'use client'
import { Suspense, useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { SetNewPasswordForm } from '@/app/(auth)/reset-password/reset-password-form'
const logger = createLogger('ResetPasswordPage')
function ResetPasswordContent() {
const router = useRouter()
const searchParams = useSearchParams()
const token = searchParams.get('token')
const [isSubmitting, setIsSubmitting] = useState(false)
const [statusMessage, setStatusMessage] = useState<{
type: 'success' | 'error' | null
text: string
}>({
type: null,
text: '',
})
useEffect(() => {
if (!token) {
setStatusMessage({
type: 'error',
text: 'Invalid or missing reset token. Please request a new password reset link.',
})
}
}, [token])
const handleResetPassword = async (password: string) => {
try {
setIsSubmitting(true)
setStatusMessage({ type: null, text: '' })
const response = await fetch('/api/auth/reset-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
token,
newPassword: password,
}),
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.message || 'Failed to reset password')
}
setStatusMessage({
type: 'success',
text: 'Password reset successful! Redirecting to login...',
})
setTimeout(() => {
router.push('/login?resetSuccess=true')
}, 1500)
} catch (error) {
logger.error('Error resetting password:', { error })
setStatusMessage({
type: 'error',
text: error instanceof Error ? error.message : 'Failed to reset password',
})
} finally {
setIsSubmitting(false)
}
}
return (
<>
<div className='space-y-1 text-center'>
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
Reset your password
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
Enter a new password for your account
</p>
</div>
<div className={`${inter.className} mt-8`}>
<SetNewPasswordForm
token={token}
onSubmit={handleResetPassword}
isSubmitting={isSubmitting}
statusType={statusMessage.type}
statusMessage={statusMessage.text}
/>
</div>
<div className={`${inter.className} pt-6 text-center font-light text-[14px]`}>
<Link
href='/login'
className='font-medium text-[var(--brand-accent-hex)] underline-offset-4 transition hover:text-[var(--brand-accent-hover-hex)] hover:underline'
>
Back to login
</Link>
</div>
</>
)
}
export default function ResetPasswordPage() {
return (
<Suspense
fallback={<div className='flex h-screen items-center justify-center'>Loading...</div>}
>
<ResetPasswordContent />
</Suspense>
)
}

View File

@@ -1,7 +1,12 @@
import type { Metadata } from 'next'
import { isRegistrationDisabled } from '@/lib/core/config/feature-flags'
import { getOAuthProviderStatus } from '@/app/(auth)/components/oauth-provider-checker'
import SignupForm from '@/app/(auth)/signup/signup-form'
export const metadata: Metadata = {
title: 'Sign Up',
}
export const dynamic = 'force-dynamic'
export default async function SignupPage() {

View File

@@ -1,7 +1,12 @@
import type { Metadata } from 'next'
import { redirect } from 'next/navigation'
import { getEnv, isTruthy } from '@/lib/core/config/env'
import SSOForm from '@/ee/sso/components/sso-form'
export const metadata: Metadata = {
title: 'Single Sign-On',
}
export const dynamic = 'force-dynamic'
export default async function SSOPage() {

View File

@@ -1,7 +1,12 @@
import type { Metadata } from 'next'
import { isEmailVerificationEnabled, isProd } from '@/lib/core/config/feature-flags'
import { hasEmailService } from '@/lib/messaging/email/mailer'
import { VerifyContent } from '@/app/(auth)/verify/verify-content'
export const metadata: Metadata = {
title: 'Verify Email',
}
export const dynamic = 'force-dynamic'
export default function VerifyPage() {

View File

@@ -43,7 +43,7 @@ function VerificationForm({
useEffect(() => {
if (countdown > 0) {
const timer = setTimeout(() => setCountdown(countdown - 1), 1000)
const timer = setTimeout(() => setCountdown((c) => c - 1), 1000)
return () => clearTimeout(timer)
}
if (countdown === 0 && isResendDisabled) {

View File

@@ -0,0 +1,332 @@
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import { Badge, ChevronDown } from '@/components/emcn'
interface DotGridProps {
className?: string
cols: number
rows: number
gap?: number
}
function DotGrid({ className, cols, rows, gap = 0 }: DotGridProps) {
return (
<div
aria-hidden='true'
className={className}
style={{
display: 'grid',
gridTemplateColumns: `repeat(${cols}, 1fr)`,
gap,
placeItems: 'center',
}}
>
{Array.from({ length: cols * rows }, (_, i) => (
<div key={i} className='h-[2px] w-[2px] rounded-full bg-[#2A2A2A]' />
))}
</div>
)
}
const CURSOR_KEYFRAMES = `
@keyframes cursorVikhyath {
0% { transform: translate(0, 0); }
12% { transform: translate(120px, 10px); }
24% { transform: translate(80px, 80px); }
36% { transform: translate(-10px, 60px); }
48% { transform: translate(-15px, -20px); }
60% { transform: translate(100px, -40px); }
72% { transform: translate(180px, 30px); }
84% { transform: translate(50px, 50px); }
100% { transform: translate(0, 0); }
}
@keyframes cursorAlexa {
0% { transform: translate(0, 0); }
14% { transform: translate(45px, -35px); }
28% { transform: translate(-75px, 20px); }
42% { transform: translate(25px, -50px); }
57% { transform: translate(-65px, 15px); }
71% { transform: translate(35px, -30px); }
85% { transform: translate(-30px, -10px); }
100% { transform: translate(0, 0); }
}
@media (prefers-reduced-motion: reduce) {
@keyframes cursorVikhyath { 0%, 100% { transform: none; } }
@keyframes cursorAlexa { 0%, 100% { transform: none; } }
}
`
const CURSOR_ARROW_PATH =
'M17.135 2.198L12.978 14.821C12.478 16.339 10.275 16.16 10.028 14.581L9.106 8.703C9.01 8.092 8.554 7.599 7.952 7.457L1.591 5.953C0 5.577 0.039 3.299 1.642 2.978L15.39 0.229C16.534 0 17.499 1.09 17.135 2.198Z'
const CURSOR_ARROW_MIRRORED_PATH =
'M0.365 2.198L4.522 14.821C5.022 16.339 7.225 16.16 7.472 14.58L8.394 8.702C8.49 8.091 8.946 7.599 9.548 7.456L15.909 5.953C17.5 5.577 17.461 3.299 15.857 2.978L2.11 0.228C0.966 0 0.001 1.09 0.365 2.198Z'
function CursorArrow({ fill }: { fill: string }) {
return (
<svg width='23.15' height='21.1' viewBox='0 0 17.5 16.4' fill='none'>
<path d={fill === '#2ABBF8' ? CURSOR_ARROW_PATH : CURSOR_ARROW_MIRRORED_PATH} fill={fill} />
</svg>
)
}
function VikhyathCursor() {
return (
<div
aria-hidden='true'
className='pointer-events-none absolute'
style={{
top: '27.47%',
left: '25%',
animation: 'cursorVikhyath 16s ease-in-out infinite',
willChange: 'transform',
}}
>
<div className='relative h-[37.14px] w-[79.18px]'>
<div className='absolute top-0 left-[56.02px]'>
<CursorArrow fill='#2ABBF8' />
</div>
<div className='-left-[4px] absolute top-[18px] flex items-center rounded bg-[#2ABBF8] px-[5px] py-[3px] font-[420] font-season text-[#202020] text-[14px] leading-[100%] tracking-[-0.02em]'>
Vikhyath
</div>
</div>
</div>
)
}
function AlexaCursor() {
return (
<div
aria-hidden='true'
className='pointer-events-none absolute'
style={{
top: '66.80%',
left: '49%',
animation: 'cursorAlexa 13s ease-in-out infinite',
willChange: 'transform',
}}
>
<div className='relative h-[35.09px] w-[62.16px]'>
<div className='absolute top-0 left-0'>
<CursorArrow fill='#FFCC02' />
</div>
<div className='absolute top-[16px] left-[23px] flex items-center rounded bg-[#FFCC02] px-[5px] py-[3px] font-[420] font-season text-[#202020] text-[14px] leading-[100%] tracking-[-0.02em]'>
Alexa
</div>
</div>
</div>
)
}
interface YouCursorProps {
x: number
y: number
visible: boolean
}
function YouCursor({ x, y, visible }: YouCursorProps) {
if (!visible) return null
return (
<div
aria-hidden='true'
className='pointer-events-none fixed z-50'
style={{
left: x,
top: y,
transform: 'translate(-2px, -2px)',
}}
>
<svg width='23.15' height='21.1' viewBox='0 0 17.5 16.4' fill='none'>
<path d={CURSOR_ARROW_MIRRORED_PATH} fill='#33C482' />
</svg>
<div className='absolute top-[16px] left-[23px] flex items-center rounded bg-[#33C482] px-[5px] py-[3px] font-[420] font-season text-[#202020] text-[14px] leading-[100%] tracking-[-0.02em]'>
You
</div>
</div>
)
}
/**
* Collaboration section — team workflows and real-time collaboration.
*
* SEO:
* - `<section id="collaboration" aria-labelledby="collaboration-heading">`.
* - `<h2 id="collaboration-heading">` for the section title.
* - Product visuals use `<figure>` with `<figcaption>` and descriptive `alt` text.
*
* GEO:
* - Name specific capabilities (version control, shared workspaces, RBAC, audit logs).
* - Lead with a summary so AI can answer "Does Sim support team collaboration?".
* - Reference "Sim" by name per capability ("Sim's real-time collaboration").
*/
const CURSOR_LERP_FACTOR = 0.3
export default function Collaboration() {
const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 })
const [isHovering, setIsHovering] = useState(false)
const sectionRef = useRef<HTMLElement>(null)
const targetPos = useRef({ x: 0, y: 0 })
const animationRef = useRef<number>(0)
useEffect(() => {
const animate = () => {
setCursorPos((prev) => ({
x: prev.x + (targetPos.current.x - prev.x) * CURSOR_LERP_FACTOR,
y: prev.y + (targetPos.current.y - prev.y) * CURSOR_LERP_FACTOR,
}))
animationRef.current = requestAnimationFrame(animate)
}
if (isHovering) {
animationRef.current = requestAnimationFrame(animate)
}
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
}
}
}, [isHovering])
const handleMouseMove = useCallback((e: React.MouseEvent) => {
targetPos.current = { x: e.clientX, y: e.clientY }
}, [])
const handleMouseEnter = useCallback((e: React.MouseEvent) => {
targetPos.current = { x: e.clientX, y: e.clientY }
setCursorPos({ x: e.clientX, y: e.clientY })
setIsHovering(true)
}, [])
const handleMouseLeave = useCallback(() => {
setIsHovering(false)
}, [])
return (
<section
ref={sectionRef}
id='collaboration'
aria-labelledby='collaboration-heading'
className='bg-[#1C1C1C]'
style={{ cursor: isHovering ? 'none' : 'auto' }}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<YouCursor x={cursorPos.x} y={cursorPos.y} visible={isHovering} />
<style dangerouslySetInnerHTML={{ __html: CURSOR_KEYFRAMES }} />
<DotGrid
className='border-[#2A2A2A] border-y bg-[#1C1C1C] p-[6px]'
cols={120}
rows={1}
gap={6}
/>
<div className='relative overflow-hidden'>
<Link
href='/studio/multiplayer'
target='_blank'
rel='noopener noreferrer'
className='absolute bottom-10 left-4 z-20 flex cursor-none items-center gap-[14px] rounded-[5px] border border-[#2A2A2A] bg-[#1C1C1C] px-[12px] py-[10px] transition-colors hover:border-[#3d3d3d] hover:bg-[#232323] sm:left-8 md:left-[80px]'
>
<div className='relative h-7 w-11 shrink-0'>
<Image src='/landing/multiplayer-cursors.svg' alt='' fill className='object-contain' />
</div>
<div className='flex flex-col gap-[2px]'>
<span className='font-[430] font-season text-[#F6F6F0]/50 text-[12px] uppercase leading-[100%] tracking-[0.08em]'>
Blog
</span>
<span className='font-[430] font-season text-[#F6F6F0] text-[14px] leading-[125%] tracking-[0.02em]'>
How we built realtime collaboration
</span>
</div>
</Link>
<div className='grid grid-cols-[auto_1fr]'>
<div className='flex flex-col items-start gap-3 px-4 pt-[100px] pb-8 sm:gap-4 sm:px-8 md:gap-[20px] md:px-[80px]'>
<Badge
variant='blue'
size='md'
dot
className='bg-[#33C482]/10 font-season text-[#33C482] uppercase tracking-[0.02em]'
>
Teams
</Badge>
<h2
id='collaboration-heading'
className='font-[430] font-season text-[32px] text-white leading-[100%] tracking-[-0.02em] sm:text-[36px] md:text-[40px]'
>
Realtime
<br />
collaboration
</h2>
<p className='font-[430] font-season text-[#F6F6F0]/50 text-[14px] leading-[125%] tracking-[0.02em] sm:text-[16px]'>
Grab your team. Build agents together <br /> in real-time inside your workspace.
</p>
<Link
href='/signup'
className='group/cta mt-[12px] inline-flex h-[32px] cursor-none items-center gap-[6px] rounded-[5px] border border-[#33C482] bg-[#33C482] px-[10px] font-[430] font-season text-[14px] text-black transition-[filter] hover:brightness-110'
>
Build together
<span className='relative h-[10px] w-[10px] shrink-0'>
<ChevronDown className='-rotate-90 absolute inset-0 h-[10px] w-[10px] transition-opacity duration-150 group-hover/cta:opacity-0' />
<svg
className='absolute inset-0 h-[10px] w-[10px] opacity-0 transition-opacity duration-150 group-hover/cta:opacity-100'
viewBox='0 0 10 10'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M1 5H8M5.5 2L8.5 5L5.5 8'
stroke='currentColor'
strokeWidth='1.33'
strokeLinecap='square'
strokeLinejoin='miter'
fill='none'
/>
</svg>
</span>
</Link>
</div>
<figure className='pointer-events-none relative h-[600px] w-full'>
<div className='-left-[18%] absolute inset-y-0 min-w-full'>
<Image
src='/landing/collaboration-visual.svg'
alt='Collaboration visual showing team workflows with real-time editing, shared cursors, and version control interface'
width={876}
height={480}
className='h-full w-auto min-w-[100vw] object-left'
priority
/>
</div>
<div className='hidden lg:block'>
<VikhyathCursor />
<AlexaCursor />
</div>
<figcaption className='sr-only'>
Sim collaboration interface with real-time cursors, shared workspace, and team
presence indicators
</figcaption>
</figure>
</div>
</div>
<DotGrid
className='border-[#2A2A2A] border-y bg-[#1C1C1C] p-[6px]'
cols={120}
rows={1}
gap={6}
/>
</section>
)
}

View File

@@ -0,0 +1,17 @@
/**
* Enterprise section — compliance, scale, and security messaging.
*
* SEO:
* - `<section id="enterprise" aria-labelledby="enterprise-heading">`.
* - `<h2 id="enterprise-heading">` for the section title.
* - Compliance certs (SOC2, HIPAA) as visible `<strong>` text.
* - Enterprise CTA links to contact form via `<a>` with `rel="noopener noreferrer"`.
*
* GEO:
* - Entity-rich: "Sim is SOC2 and HIPAA compliant" — not "We are compliant."
* - `<ul>` checklist of features (SSO, RBAC, audit logs, SLA, on-premise deployment)
* as an atomic answer block for "What enterprise features does Sim offer?".
*/
export default function Enterprise() {
return null
}

View File

@@ -0,0 +1,229 @@
'use client'
import { useState } from 'react'
import Image from 'next/image'
import { Badge } from '@/components/emcn'
function hexToRgba(hex: string, alpha: number): string {
const r = Number.parseInt(hex.slice(1, 3), 16)
const g = Number.parseInt(hex.slice(3, 5), 16)
const b = Number.parseInt(hex.slice(5, 7), 16)
return `rgba(${r},${g},${b},${alpha})`
}
const FEATURE_TABS = [
{
label: 'Integrations',
color: '#FA4EDF',
segments: [
[0.3, 8],
[0.25, 10],
[0.45, 12],
[0.5, 8],
[0.65, 10],
[0.8, 12],
[0.75, 8],
[0.95, 10],
[1, 12],
[0.85, 10],
],
},
{
label: 'Copilot',
color: '#2ABBF8',
segments: [
[0.25, 12],
[0.4, 10],
[0.35, 8],
[0.55, 12],
[0.7, 10],
[0.85, 8],
[1, 14],
[0.9, 12],
[1, 14],
],
},
{
label: 'Models',
color: '#00F701',
badgeColor: '#22C55E',
segments: [
[0.2, 6],
[0.35, 10],
[0.3, 8],
[0.5, 10],
[0.6, 8],
[0.75, 12],
[0.85, 10],
[1, 8],
[0.9, 12],
[1, 10],
[0.95, 6],
],
},
{
label: 'Deploy',
color: '#FFCC02',
badgeColor: '#EAB308',
segments: [
[0.3, 12],
[0.25, 8],
[0.4, 10],
[0.55, 10],
[0.7, 8],
[0.6, 10],
[0.85, 12],
[1, 10],
[0.9, 10],
[1, 10],
],
},
{
label: 'Logs',
color: '#FF6B35',
segments: [
[0.25, 10],
[0.35, 8],
[0.3, 10],
[0.5, 10],
[0.65, 8],
[0.8, 12],
[0.9, 10],
[1, 10],
[0.85, 12],
[1, 10],
],
},
{
label: 'Knowledge Base',
color: '#8B5CF6',
segments: [
[0.3, 10],
[0.25, 8],
[0.4, 10],
[0.5, 10],
[0.65, 10],
[0.8, 10],
[0.9, 12],
[1, 10],
[0.95, 10],
[1, 10],
],
},
]
function DotGrid({
cols,
rows,
width,
borderLeft,
}: {
cols: number
rows: number
width?: number
borderLeft?: boolean
}) {
return (
<div
aria-hidden='true'
className={`shrink-0 bg-[#FDFDFD] p-[6px] ${borderLeft ? 'border-[#E9E9E9] border-l' : ''}`}
style={{
width: width ? `${width}px` : undefined,
display: 'grid',
gridTemplateColumns: `repeat(${cols}, 1fr)`,
gap: 4,
placeItems: 'center',
}}
>
{Array.from({ length: cols * rows }, (_, i) => (
<div key={i} className='h-[2px] w-[2px] rounded-full bg-[#DEDEDE]' />
))}
</div>
)
}
export default function Features() {
const [activeTab, setActiveTab] = useState(0)
return (
<section
id='features'
aria-labelledby='features-heading'
className='relative overflow-hidden bg-[#F6F6F6] pb-[144px]'
>
<div aria-hidden='true' className='absolute top-0 left-0 w-full'>
<Image
src='/landing/features-transition.svg'
alt=''
width={1440}
height={366}
className='h-auto w-full'
priority
/>
</div>
<div className='relative z-10 pt-[100px]'>
<div className='flex flex-col items-start gap-[20px] px-[80px]'>
<Badge
variant='blue'
size='md'
dot
className='font-season uppercase tracking-[0.02em] transition-colors duration-200'
style={{
color: FEATURE_TABS[activeTab].badgeColor ?? FEATURE_TABS[activeTab].color,
backgroundColor: hexToRgba(
FEATURE_TABS[activeTab].badgeColor ?? FEATURE_TABS[activeTab].color,
0.1
),
}}
>
Features
</Badge>
<h2
id='features-heading'
className='font-[430] font-season text-[#1C1C1C] text-[40px] leading-[100%] tracking-[-0.02em]'
>
Power your AI workforce
</h2>
</div>
<div className='mt-[73px] flex h-[68px] overflow-hidden border border-[#E9E9E9]'>
<DotGrid cols={10} rows={8} width={80} />
<div role='tablist' aria-label='Feature categories' className='flex flex-1'>
{FEATURE_TABS.map((tab, index) => (
<button
key={tab.label}
type='button'
role='tab'
aria-selected={index === activeTab}
onClick={() => setActiveTab(index)}
className='relative flex h-full flex-1 items-center justify-center border-[#E9E9E9] border-l font-medium font-season text-[#212121] text-[14px] uppercase'
style={{ backgroundColor: index === activeTab ? '#FDFDFD' : '#F6F6F6' }}
>
{tab.label}
{index === activeTab && (
<div className='absolute right-0 bottom-0 left-0 flex h-[6px]'>
{tab.segments.map(([opacity, width], i) => (
<div
key={i}
className='h-full shrink-0'
style={{
width: `${width}%`,
backgroundColor: tab.color,
opacity,
}}
/>
))}
</div>
)}
</button>
))}
</div>
<DotGrid cols={10} rows={8} width={80} borderLeft />
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,18 @@
/**
* Landing page footer — navigation, legal links, and entity reinforcement.
*
* SEO:
* - `<footer role="contentinfo">` with `<nav aria-label="Footer navigation">`.
* - Link groups under semantic headings (`<h3>`). All links are `<Link>` or `<a>` with `href`.
* - External links include `rel="noopener noreferrer"`.
* - Legal links (Privacy, Terms) must be crawlable (trust signals).
*
* GEO:
* - Include "Sim — Build AI agents and run your agentic workforce" as visible text (entity reinforcement).
* - Social links (X, GitHub, LinkedIn, Discord) must match `sameAs` in structured-data.tsx.
* - Link to all major pages: Docs, Pricing, Enterprise, Careers, Changelog (internal link graph).
* - Display compliance badges (SOC2, HIPAA) and status page link as visible trust signals.
*/
export default function Footer() {
return null
}

View File

@@ -0,0 +1,584 @@
'use client'
import { useEffect, useState } from 'react'
import { motion, type Variants } from 'framer-motion'
/** Stagger between each block appearing (seconds). */
const ENTER_STAGGER = 0.06
/** Duration of each block's fade-in (seconds). */
const ENTER_DURATION = 0.3
/** Stagger between each block disappearing (seconds). */
const EXIT_STAGGER = 0.12
/** Duration of each block's fade-out (seconds). */
const EXIT_DURATION = 0.5
/** Shared corner radius for all decorative rects. */
const RX = '2.59574'
/** Hold time after the initial enter animation before cycling starts (ms). */
const INITIAL_HOLD_MS = 2500
/** Pause between an exit completing and the next enter starting (ms). */
const TRANSITION_PAUSE_MS = 400
/** Hold time between successive transitions (ms). */
const HOLD_BETWEEN_MS = 2500
/** Animation state for a block group. */
export type BlockAnimState = 'entering' | 'visible' | 'exiting' | 'hidden'
/** Positions around the hero where block groups can appear. */
export type BlockPosition = 'topRight' | 'left' | 'rightEdge' | 'rightSide' | 'topLeft'
/** Attributes for a single animated SVG rect. */
interface BlockRect {
opacity: number
width: string
height: string
fill: string
x?: string
y?: string
transform?: string
}
const containerVariants: Variants = {
hidden: {},
visible: { transition: { staggerChildren: ENTER_STAGGER } },
exit: { transition: { staggerChildren: EXIT_STAGGER } },
}
const blockVariants: Variants = {
hidden: { opacity: 0, transition: { duration: 0 } },
visible: (targetOpacity: number) => ({
opacity: targetOpacity,
transition: { duration: ENTER_DURATION },
}),
exit: {
opacity: 0,
transition: { duration: EXIT_DURATION },
},
}
/** Maps a BlockAnimState to the framer-motion animate value. */
function toAnimateValue(state: BlockAnimState): string {
if (state === 'entering' || state === 'visible') return 'visible'
if (state === 'exiting') return 'exit'
return 'hidden'
}
/** Shared SVG wrapper that staggers child rects in and out. */
function AnimatedBlocksSvg({
width,
height,
viewBox,
rects,
animState = 'entering',
}: {
width: number
height: number
viewBox: string
rects: readonly BlockRect[]
animState?: BlockAnimState
}) {
return (
<motion.svg
width={width}
height={height}
viewBox={viewBox}
fill='none'
xmlns='http://www.w3.org/2000/svg'
className='h-auto w-full'
initial='hidden'
animate={toAnimateValue(animState)}
variants={containerVariants}
>
{rects.map((r, i) => (
<motion.rect
key={i}
variants={blockVariants}
custom={r.opacity}
x={r.x}
y={r.y}
width={r.width}
height={r.height}
rx={RX}
fill={r.fill}
transform={r.transform}
/>
))}
</motion.svg>
)
}
/**
* Rect data for the top-right position.
* Two-row horizontal strip, ordered left-to-right.
*/
const TOP_RIGHT_RECTS: readonly BlockRect[] = [
{ opacity: 1, x: '0', y: '0', width: '16.8626', height: '33.7252', fill: '#2ABBF8' },
{ opacity: 0.6, x: '0', y: '0', width: '85.3433', height: '16.8626', fill: '#2ABBF8' },
{ opacity: 1, x: '0', y: '0', width: '16.8626', height: '16.8626', fill: '#2ABBF8' },
{ opacity: 0.6, x: '34.2403', y: '0', width: '34.2403', height: '33.7252', fill: '#2ABBF8' },
{ opacity: 1, x: '34.2403', y: '0', width: '16.8626', height: '16.8626', fill: '#2ABBF8' },
{ opacity: 1, x: '51.6188', y: '16.8626', width: '16.8626', height: '16.8626', fill: '#2ABBF8' },
{ opacity: 1, x: '68.4812', y: '0', width: '54.6502', height: '16.8626', fill: '#00F701' },
{ opacity: 0.6, x: '106.268', y: '0', width: '34.2403', height: '33.7252', fill: '#00F701' },
{ opacity: 0.6, x: '106.268', y: '0', width: '51.103', height: '16.8626', fill: '#00F701' },
{ opacity: 1, x: '123.6484', y: '16.8626', width: '16.8626', height: '16.8626', fill: '#00F701' },
{ opacity: 0.6, x: '157.371', y: '0', width: '34.2403', height: '16.8626', fill: '#FFCC02' },
{ opacity: 1, x: '157.371', y: '0', width: '16.8626', height: '16.8626', fill: '#FFCC02' },
{ opacity: 0.6, x: '208.993', y: '0', width: '68.4805', height: '16.8626', fill: '#FA4EDF' },
{ opacity: 0.6, x: '209.137', y: '0', width: '16.8626', height: '33.7252', fill: '#FA4EDF' },
{ opacity: 0.6, x: '243.233', y: '0', width: '34.2403', height: '33.7252', fill: '#FA4EDF' },
{ opacity: 1, x: '243.233', y: '0', width: '16.8626', height: '16.8626', fill: '#FA4EDF' },
{ opacity: 0.6, x: '260.096', y: '0', width: '34.04', height: '16.8626', fill: '#FA4EDF' },
{ opacity: 1, x: '260.611', y: '16.8626', width: '16.8626', height: '16.8626', fill: '#FA4EDF' },
]
/**
* Rect data for the top-left position.
* Same two-row structure as top-right with rotated colour palette:
* blue→green, green→yellow, yellow→pink, pink→blue.
*/
const TOP_LEFT_RECTS: readonly BlockRect[] = [
{ opacity: 1, x: '0', y: '0', width: '16.8626', height: '33.7252', fill: '#00F701' },
{ opacity: 0.6, x: '0', y: '0', width: '85.3433', height: '16.8626', fill: '#00F701' },
{ opacity: 1, x: '0', y: '0', width: '16.8626', height: '16.8626', fill: '#00F701' },
{ opacity: 0.6, x: '34.2403', y: '0', width: '34.2403', height: '33.7252', fill: '#00F701' },
{ opacity: 1, x: '34.2403', y: '0', width: '16.8626', height: '16.8626', fill: '#00F701' },
{ opacity: 1, x: '51.6188', y: '16.8626', width: '16.8626', height: '16.8626', fill: '#00F701' },
{ opacity: 1, x: '68.4812', y: '0', width: '54.6502', height: '16.8626', fill: '#FFCC02' },
{ opacity: 0.6, x: '106.268', y: '0', width: '34.2403', height: '33.7252', fill: '#FFCC02' },
{ opacity: 0.6, x: '106.268', y: '0', width: '51.103', height: '16.8626', fill: '#FFCC02' },
{ opacity: 1, x: '123.6484', y: '16.8626', width: '16.8626', height: '16.8626', fill: '#FFCC02' },
{ opacity: 0.6, x: '157.371', y: '0', width: '34.2403', height: '16.8626', fill: '#FA4EDF' },
{ opacity: 1, x: '157.371', y: '0', width: '16.8626', height: '16.8626', fill: '#FA4EDF' },
{ opacity: 0.6, x: '208.993', y: '0', width: '68.4805', height: '16.8626', fill: '#2ABBF8' },
{ opacity: 0.6, x: '209.137', y: '0', width: '16.8626', height: '33.7252', fill: '#2ABBF8' },
{ opacity: 0.6, x: '243.233', y: '0', width: '34.2403', height: '33.7252', fill: '#2ABBF8' },
{ opacity: 1, x: '243.233', y: '0', width: '16.8626', height: '16.8626', fill: '#2ABBF8' },
{ opacity: 0.6, x: '260.096', y: '0', width: '34.04', height: '16.8626', fill: '#2ABBF8' },
{ opacity: 1, x: '260.611', y: '16.8626', width: '16.8626', height: '16.8626', fill: '#2ABBF8' },
]
/**
* Rect data for the left position.
* Two-column vertical strip, ordered top-to-bottom.
*/
const LEFT_RECTS: readonly BlockRect[] = [
{
opacity: 0.6,
width: '34.240',
height: '33.725',
fill: '#FA4EDF',
transform: 'matrix(0 1 1 0 0 0)',
},
{
opacity: 0.6,
width: '16.8626',
height: '68.480',
fill: '#FA4EDF',
transform: 'matrix(-1 0 0 1 33.727 0)',
},
{
opacity: 1,
width: '16.8626',
height: '16.8626',
fill: '#FA4EDF',
transform: 'matrix(-1 0 0 1 33.727 17.378)',
},
{
opacity: 0.6,
width: '16.8626',
height: '33.986',
fill: '#FA4EDF',
transform: 'matrix(0 1 1 0 0 51.616)',
},
{
opacity: 0.6,
width: '16.8626',
height: '140.507',
fill: '#00F701',
transform: 'matrix(-1 0 0 1 33.986 85.335)',
},
{
opacity: 0.4,
x: '17.119',
y: '136.962',
width: '34.240',
height: '16.8626',
fill: '#FFCC02',
transform: 'rotate(-90 17.119 136.962)',
},
{
opacity: 1,
x: '17.119',
y: '136.962',
width: '16.8626',
height: '16.8626',
fill: '#FFCC02',
transform: 'rotate(-90 17.119 136.962)',
},
{
opacity: 0.5,
width: '34.240',
height: '33.725',
fill: '#00F701',
transform: 'matrix(0 1 1 0 0.257 153.825)',
},
{
opacity: 1,
width: '16.8626',
height: '16.8626',
fill: '#00F701',
transform: 'matrix(0 1 1 0 0.257 153.825)',
},
]
/**
* Rect data for the right-side position (right edge of screenshot).
* Same two-column structure as left with rotated colours:
* pink→blue, green→pink, yellow→green.
*/
const RIGHT_SIDE_RECTS: readonly BlockRect[] = [
{
opacity: 0.6,
width: '34.240',
height: '33.725',
fill: '#2ABBF8',
transform: 'matrix(0 1 1 0 0 0)',
},
{
opacity: 0.6,
width: '16.8626',
height: '68.480',
fill: '#2ABBF8',
transform: 'matrix(-1 0 0 1 33.727 0)',
},
{
opacity: 1,
width: '16.8626',
height: '16.8626',
fill: '#2ABBF8',
transform: 'matrix(-1 0 0 1 33.727 17.378)',
},
{
opacity: 0.6,
width: '16.8626',
height: '33.986',
fill: '#2ABBF8',
transform: 'matrix(0 1 1 0 0 51.616)',
},
{
opacity: 0.6,
width: '16.8626',
height: '140.507',
fill: '#FA4EDF',
transform: 'matrix(-1 0 0 1 33.986 85.335)',
},
{
opacity: 0.4,
x: '17.119',
y: '136.962',
width: '34.240',
height: '16.8626',
fill: '#00F701',
transform: 'rotate(-90 17.119 136.962)',
},
{
opacity: 1,
x: '17.119',
y: '136.962',
width: '16.8626',
height: '16.8626',
fill: '#00F701',
transform: 'rotate(-90 17.119 136.962)',
},
{
opacity: 0.5,
width: '34.240',
height: '33.725',
fill: '#FA4EDF',
transform: 'matrix(0 1 1 0 0.257 153.825)',
},
{
opacity: 1,
width: '16.8626',
height: '16.8626',
fill: '#FA4EDF',
transform: 'matrix(0 1 1 0 0.257 153.825)',
},
]
/**
* Rect data for the right-edge position (far right of screen).
* Two-column vertical strip, ordered top-to-bottom.
*/
const RIGHT_RECTS: readonly BlockRect[] = [
{
opacity: 0.6,
width: '16.8626',
height: '33.726',
fill: '#FA4EDF',
transform: 'matrix(0 1 1 0 0 0)',
},
{
opacity: 0.6,
width: '34.241',
height: '16.8626',
fill: '#FA4EDF',
transform: 'matrix(0 1 1 0 16.891 0)',
},
{
opacity: 0.6,
width: '16.8626',
height: '68.482',
fill: '#FA4EDF',
transform: 'matrix(-1 0 0 1 33.739 16.888)',
},
{
opacity: 0.6,
width: '16.8626',
height: '33.726',
fill: '#FA4EDF',
transform: 'matrix(0 1 1 0 0 33.776)',
},
{
opacity: 1,
width: '16.8626',
height: '16.8626',
fill: '#FA4EDF',
transform: 'matrix(-1 0 0 1 33.739 34.272)',
},
{
opacity: 0.6,
width: '16.8626',
height: '33.726',
fill: '#FA4EDF',
transform: 'matrix(0 1 1 0 0.012 68.510)',
},
{
opacity: 0.6,
width: '16.8626',
height: '102.384',
fill: '#2ABBF8',
transform: 'matrix(-1 0 0 1 33.787 102.384)',
},
{
opacity: 0.4,
x: '17.131',
y: '153.859',
width: '34.241',
height: '16.8626',
fill: '#00F701',
transform: 'rotate(-90 17.131 153.859)',
},
{
opacity: 1,
x: '17.131',
y: '153.859',
width: '16.8626',
height: '16.8626',
fill: '#00F701',
transform: 'rotate(-90 17.131 153.859)',
},
]
/** Number of rects per position, used to compute animation durations. */
const RECT_COUNTS: Record<BlockPosition, number> = {
topRight: TOP_RIGHT_RECTS.length,
topLeft: TOP_LEFT_RECTS.length,
left: LEFT_RECTS.length,
rightSide: RIGHT_SIDE_RECTS.length,
rightEdge: RIGHT_RECTS.length,
}
/** Total enter animation time for a position (seconds). */
function enterTime(pos: BlockPosition): number {
return (RECT_COUNTS[pos] - 1) * ENTER_STAGGER + ENTER_DURATION
}
/** Total exit animation time for a position (seconds). */
function exitTime(pos: BlockPosition): number {
return (RECT_COUNTS[pos] - 1) * EXIT_STAGGER + EXIT_DURATION
}
/** A single step in the repeating animation cycle. */
type CycleStep =
| { action: 'exit'; position: BlockPosition }
| { action: 'enter'; position: BlockPosition }
| { action: 'hold'; ms: number }
/**
* The repeating cycle sequence. After all steps, the layout returns to its
* initial state (topRight + left + rightEdge) so the loop is seamless.
*
* Order: exit top → exit right-edge → enter right-side-of-preview →
* exit left → enter top-left → exit right-side → enter left →
* exit top-left → enter top-right → enter right-edge → back to initial.
*/
const CYCLE_STEPS: readonly CycleStep[] = [
{ action: 'exit', position: 'topRight' },
{ action: 'exit', position: 'rightEdge' },
{ action: 'enter', position: 'rightSide' },
{ action: 'hold', ms: HOLD_BETWEEN_MS },
{ action: 'exit', position: 'left' },
{ action: 'enter', position: 'topLeft' },
{ action: 'hold', ms: HOLD_BETWEEN_MS },
{ action: 'exit', position: 'rightSide' },
{ action: 'enter', position: 'left' },
{ action: 'hold', ms: HOLD_BETWEEN_MS },
{ action: 'exit', position: 'topLeft' },
{ action: 'enter', position: 'topRight' },
{ action: 'hold', ms: HOLD_BETWEEN_MS },
{ action: 'enter', position: 'rightEdge' },
{ action: 'hold', ms: HOLD_BETWEEN_MS },
]
/**
* Drives the block-cycling animation loop. Returns the current animation
* state for every position so each component can be driven declaratively.
*
* Lifecycle:
* 1. All three initial groups (topRight, left, rightEdge) enter together.
* 2. After a hold period the cycle begins, processing each step in order.
* 3. Repeats indefinitely, returning to the initial layout every cycle.
*/
export function useBlockCycle(): Record<BlockPosition, BlockAnimState> {
const [states, setStates] = useState<Record<BlockPosition, BlockAnimState>>({
topRight: 'entering',
left: 'entering',
rightEdge: 'entering',
rightSide: 'hidden',
topLeft: 'hidden',
})
useEffect(() => {
const cancelled = { current: false }
const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
const run = async () => {
const longestEnter = Math.max(
enterTime('topRight'),
enterTime('left'),
enterTime('rightEdge')
)
await delay(longestEnter * 1000)
if (cancelled.current) return
setStates({
topRight: 'visible',
left: 'visible',
rightEdge: 'visible',
rightSide: 'hidden',
topLeft: 'hidden',
})
await delay(INITIAL_HOLD_MS)
if (cancelled.current) return
while (!cancelled.current) {
for (const step of CYCLE_STEPS) {
if (cancelled.current) return
if (step.action === 'exit') {
setStates((prev) => ({ ...prev, [step.position]: 'exiting' }))
await delay(exitTime(step.position) * 1000)
if (cancelled.current) return
setStates((prev) => ({ ...prev, [step.position]: 'hidden' }))
await delay(TRANSITION_PAUSE_MS)
} else if (step.action === 'enter') {
setStates((prev) => ({ ...prev, [step.position]: 'entering' }))
await delay(enterTime(step.position) * 1000)
if (cancelled.current) return
setStates((prev) => ({ ...prev, [step.position]: 'visible' }))
await delay(TRANSITION_PAUSE_MS)
} else {
await delay(step.ms)
}
if (cancelled.current) return
}
}
}
run()
return () => {
cancelled.current = true
}
}, [])
return states
}
interface AnimatedBlockProps {
animState?: BlockAnimState
}
/** Two-row horizontal strip at the top-right of the hero. */
export function BlocksTopRightAnimated({ animState = 'entering' }: AnimatedBlockProps) {
return (
<AnimatedBlocksSvg
width={295}
height={34}
viewBox='0 0 295 34'
rects={TOP_RIGHT_RECTS}
animState={animState}
/>
)
}
/** Two-row horizontal strip at the top-left of the hero. */
export function BlocksTopLeftAnimated({ animState = 'entering' }: AnimatedBlockProps) {
return (
<AnimatedBlocksSvg
width={295}
height={34}
viewBox='0 0 295 34'
rects={TOP_LEFT_RECTS}
animState={animState}
/>
)
}
/** Two-column vertical strip on the left edge of the screenshot. */
export function BlocksLeftAnimated({ animState = 'entering' }: AnimatedBlockProps) {
return (
<AnimatedBlocksSvg
width={34}
height={226}
viewBox='0 0 34 226.021'
rects={LEFT_RECTS}
animState={animState}
/>
)
}
/** Two-column vertical strip on the right edge of the screenshot. */
export function BlocksRightSideAnimated({ animState = 'entering' }: AnimatedBlockProps) {
return (
<AnimatedBlocksSvg
width={34}
height={226}
viewBox='0 0 34 226.021'
rects={RIGHT_SIDE_RECTS}
animState={animState}
/>
)
}
/** Two-column vertical strip at the far-right edge of the screen. */
export function BlocksRightAnimated({ animState = 'entering' }: AnimatedBlockProps) {
return (
<AnimatedBlocksSvg
width={34}
height={205}
viewBox='0 0 34 204.769'
rects={RIGHT_RECTS}
animState={animState}
/>
)
}

View File

@@ -0,0 +1,132 @@
'use client'
import dynamic from 'next/dynamic'
import Image from 'next/image'
import Link from 'next/link'
import {
BlocksLeftAnimated,
BlocksRightAnimated,
BlocksRightSideAnimated,
BlocksTopLeftAnimated,
BlocksTopRightAnimated,
useBlockCycle,
} from '@/app/(home)/components/hero/components/animated-blocks'
const LandingPreview = dynamic(
() =>
import('@/app/(home)/components/landing-preview/landing-preview').then(
(mod) => mod.LandingPreview
),
{
ssr: false,
loading: () => <div className='aspect-[1116/549] w-full rounded bg-[#1b1b1b]' />,
}
)
/** Shared base classes for CTA link buttons — matches Deploy/Run button styling in the preview panel. */
const CTA_BASE =
'inline-flex items-center h-[32px] rounded-[5px] border px-[10px] font-[430] font-season text-[14px]'
export default function Hero() {
const blockStates = useBlockCycle()
return (
<section
id='hero'
aria-labelledby='hero-heading'
className='relative flex flex-col items-center overflow-hidden bg-[#1C1C1C] pt-[71px]'
>
<p className='sr-only'>
Sim is the open-source platform to build AI agents and run your agentic workforce. Connect
1,000+ integrations and LLMs including OpenAI, Claude, Gemini, Mistral, and xAI to
deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables,
and docs. Trusted by over 100,000 builders at startups and Fortune 500 companies. SOC2 and
HIPAA compliant.
</p>
<div
aria-hidden='true'
className='pointer-events-none absolute top-[-0.7vw] left-[-2.8vw] z-0 aspect-[344/328] w-[23.9vw]'
>
<Image src='/landing/card-left.svg' alt='' fill className='object-contain' />
</div>
<div
aria-hidden='true'
className='pointer-events-none absolute top-[-2.8vw] right-[0vw] z-0 aspect-[471/470] w-[32.7vw]'
>
<Image src='/landing/card-right.svg' alt='' fill className='object-contain' />
</div>
<div className='relative z-10 flex flex-col items-center gap-[12px]'>
<h1
id='hero-heading'
className='font-[430] font-season text-[64px] text-white leading-[100%] tracking-[-0.02em]'
>
Build Agents
</h1>
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[16px] leading-[125%] tracking-[0.02em]'>
Build and deploy agentic workflows
</p>
<div className='mt-[12px] flex items-center gap-[8px]'>
<Link
href='/login'
className={`${CTA_BASE} border-[#3d3d3d] text-[#ECECEC] transition-colors hover:bg-[#2A2A2A]`}
aria-label='Log in'
>
Log in
</Link>
<Link
href='/signup'
className={`${CTA_BASE} gap-[8px] border-[#33C482] bg-[#33C482] text-black transition-[filter] hover:brightness-110`}
aria-label='Get started with Sim'
>
Get started
</Link>
</div>
</div>
<div
aria-hidden='true'
className='pointer-events-none absolute top-0 right-[13.1vw] z-20 w-[calc(140px_+_10.76vw)] max-w-[295px]'
>
<BlocksTopRightAnimated animState={blockStates.topRight} />
</div>
<div
aria-hidden='true'
className='pointer-events-none absolute top-0 left-[16vw] z-20 w-[calc(140px_+_10.76vw)] max-w-[295px]'
>
<BlocksTopLeftAnimated animState={blockStates.topLeft} />
</div>
<div className='relative z-10 mx-auto mt-[2.4vw] w-[78.9vw] px-[1.4vw]'>
<div
aria-hidden='true'
className='-translate-y-1/2 pointer-events-none absolute top-[50%] right-[calc(100%-1.41vw)] z-20 w-[calc(16px_+_1.25vw)] max-w-[34px]'
>
<BlocksLeftAnimated animState={blockStates.left} />
</div>
<div
aria-hidden='true'
className='-translate-y-1/2 pointer-events-none absolute top-[50%] left-[calc(100%-1.41vw)] z-20 w-[calc(16px_+_1.25vw)] max-w-[34px] scale-x-[-1]'
>
<BlocksRightSideAnimated animState={blockStates.rightSide} />
</div>
<div className='relative z-10 overflow-hidden rounded border border-[#2A2A2A]'>
<LandingPreview />
</div>
</div>
<div
aria-hidden='true'
className='-translate-y-1/2 pointer-events-none absolute top-[50%] right-0 z-20 w-[calc(16px_+_1.25vw)] max-w-[34px]'
>
<BlocksRightAnimated animState={blockStates.rightEdge} />
</div>
</section>
)
}

View File

@@ -0,0 +1,23 @@
import Collaboration from '@/app/(home)/components/collaboration/collaboration'
import Enterprise from '@/app/(home)/components/enterprise/enterprise'
import Features from '@/app/(home)/components/features/features'
import Footer from '@/app/(home)/components/footer/footer'
import Hero from '@/app/(home)/components/hero/hero'
import Navbar from '@/app/(home)/components/navbar/navbar'
import Pricing from '@/app/(home)/components/pricing/pricing'
import StructuredData from '@/app/(home)/components/structured-data'
import Templates from '@/app/(home)/components/templates/templates'
import Testimonials from '@/app/(home)/components/testimonials/testimonials'
export {
Collaboration,
Enterprise,
Features,
Footer,
Hero,
Navbar,
Pricing,
StructuredData,
Templates,
Testimonials,
}

View File

@@ -0,0 +1,153 @@
'use client'
import { memo, useCallback, useRef, useState } from 'react'
import { ArrowUp } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { createPortal } from 'react-dom'
import { BubbleChatPreview, ChevronDown, MoreHorizontal, Play } from '@/components/emcn'
import { LandingPromptStorage } from '@/lib/core/utils/browser-storage'
/**
* Lightweight static panel replicating the real workspace panel styling.
* The copilot tab is active with a functional user input.
* When submitted, stores the prompt and redirects to /signup (same as landing hero).
*
* Structure mirrors the real Panel component:
* aside > div.border-l.pt-[14px] > Header(px-8) > Tabs(px-8,pt-14) > Content(pt-12)
* inside Content > Copilot > header-bar(mx-[-1px]) > UserInput(p-8)
*/
export const LandingPreviewPanel = memo(function LandingPreviewPanel() {
const router = useRouter()
const [inputValue, setInputValue] = useState('')
const textareaRef = useRef<HTMLTextAreaElement>(null)
const [cursorPos, setCursorPos] = useState<{ x: number; y: number } | null>(null)
const isEmpty = inputValue.trim().length === 0
const handleSubmit = useCallback(() => {
if (isEmpty) return
LandingPromptStorage.store(inputValue)
router.push('/signup')
}, [isEmpty, inputValue, router])
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSubmit()
}
},
[handleSubmit]
)
return (
<div className='flex h-full w-[280px] flex-shrink-0 flex-col bg-[#1e1e1e]'>
<div className='flex h-full flex-col border-[#2c2c2c] border-l pt-[14px]'>
{/* Header — More + Chat | Deploy + Run */}
<div className='flex flex-shrink-0 items-center justify-between px-[8px]'>
<div className='pointer-events-none flex gap-[6px]'>
<div className='flex h-[30px] w-[30px] items-center justify-center rounded-[5px] border border-[#3d3d3d] bg-[#363636]'>
<MoreHorizontal className='h-[14px] w-[14px] text-[#e6e6e6]' />
</div>
<div className='flex h-[30px] w-[30px] items-center justify-center rounded-[5px] border border-[#3d3d3d] bg-[#363636]'>
<BubbleChatPreview className='h-[14px] w-[14px] text-[#e6e6e6]' />
</div>
</div>
<Link
href='/signup'
className='flex gap-[6px]'
onMouseMove={(e) => setCursorPos({ x: e.clientX, y: e.clientY })}
onMouseLeave={() => setCursorPos(null)}
>
<div className='flex h-[30px] items-center rounded-[5px] bg-[#32bd7e] px-[10px] transition-[filter] hover:brightness-110'>
<span className='font-medium text-[#1b1b1b] text-[12px]'>Deploy</span>
</div>
<div className='flex h-[30px] items-center gap-[8px] rounded-[5px] bg-[#32bd7e] px-[10px] transition-[filter] hover:brightness-110'>
<Play className='h-[11.5px] w-[11.5px] text-[#1b1b1b]' />
<span className='font-medium text-[#1b1b1b] text-[12px]'>Run</span>
</div>
</Link>
{cursorPos &&
createPortal(
<div
className='pointer-events-none fixed z-[9999]'
style={{ left: cursorPos.x + 14, top: cursorPos.y + 14 }}
>
{/* Decorative color bars — mirrors hero top-right block sequence */}
<div className='flex h-[4px]'>
<div className='h-full w-[8px] bg-[#2ABBF8]' />
<div className='h-full w-[14px] bg-[#2ABBF8] opacity-60' />
<div className='h-full w-[8px] bg-[#00F701]' />
<div className='h-full w-[16px] bg-[#00F701] opacity-60' />
<div className='h-full w-[8px] bg-[#FFCC02]' />
<div className='h-full w-[10px] bg-[#FFCC02] opacity-60' />
<div className='h-full w-[8px] bg-[#FA4EDF]' />
<div className='h-full w-[14px] bg-[#FA4EDF] opacity-60' />
</div>
<div className='flex items-center gap-[5px] bg-white px-[6px] py-[4px] font-medium text-[#1C1C1C] text-[11px]'>
Get started
<ChevronDown className='-rotate-90 h-[7px] w-[7px] text-[#1C1C1C]' />
</div>
</div>,
document.body
)}
</div>
{/* Tabs */}
<div className='flex flex-shrink-0 items-center px-[8px] pt-[14px]'>
<div className='pointer-events-none flex gap-[4px]'>
<div className='flex h-[28px] items-center rounded-[6px] border border-[#3d3d3d] bg-[#363636] px-[8px] py-[5px]'>
<span className='font-medium text-[#e6e6e6] text-[12.5px]'>Copilot</span>
</div>
<div className='flex h-[28px] items-center rounded-[6px] border border-transparent px-[8px] py-[5px]'>
<span className='font-medium text-[#787878] text-[12.5px]'>Toolbar</span>
</div>
<div className='flex h-[28px] items-center rounded-[6px] border border-transparent px-[8px] py-[5px]'>
<span className='font-medium text-[#787878] text-[12.5px]'>Editor</span>
</div>
</div>
</div>
{/* Tab content — copilot */}
<div className='flex flex-1 flex-col overflow-hidden pt-[12px]'>
<div className='flex h-full flex-col'>
{/* Copilot header bar — matches mx-[-1px] in real copilot */}
<div className='pointer-events-none mx-[-1px] flex flex-shrink-0 items-center rounded-[4px] border border-[#2c2c2c] bg-[#292929] px-[12px] py-[6px]'>
<span className='truncate font-medium text-[#e6e6e6] text-[14px]'>New Chat</span>
</div>
{/* User input — matches real UserInput at p-[8px] inside copilot welcome state */}
<div className='px-[8px] pt-[12px] pb-[8px]'>
<div className='rounded-[4px] border border-[#3d3d3d] bg-[#292929] px-[6px] py-[6px]'>
<textarea
ref={textareaRef}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder='Build an AI agent...'
rows={2}
className='mb-[6px] min-h-[48px] w-full cursor-text resize-none border-0 bg-transparent px-[2px] py-1 font-base text-[#e6e6e6] text-sm leading-[1.25rem] placeholder-[#787878] caret-[#e6e6e6] outline-none'
/>
<div className='flex items-center justify-end'>
<button
type='button'
onClick={handleSubmit}
disabled={isEmpty}
className='flex h-[22px] w-[22px] items-center justify-center rounded-full border-0 p-0 transition-colors'
style={{
background: isEmpty ? '#808080' : '#e0e0e0',
cursor: isEmpty ? 'not-allowed' : 'pointer',
}}
>
<ArrowUp size={14} strokeWidth={2.25} color='#1b1b1b' />
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
})

View File

@@ -0,0 +1,142 @@
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Database, Layout, Search, Settings } from 'lucide-react'
import { ChevronDown, Library } from '@/components/emcn'
import type { PreviewWorkflow } from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
/**
* Props for the LandingPreviewSidebar component
*/
interface LandingPreviewSidebarProps {
workflows: PreviewWorkflow[]
activeWorkflowId: string
onSelectWorkflow: (id: string) => void
}
/**
* Static footer navigation items matching the real sidebar
*/
const FOOTER_NAV_ITEMS = [
{ id: 'logs', label: 'Logs', icon: Library },
{ id: 'templates', label: 'Templates', icon: Layout },
{ id: 'knowledge-base', label: 'Knowledge Base', icon: Database },
{ id: 'settings', label: 'Settings', icon: Settings },
] as const
/**
* Lightweight static sidebar replicating the real workspace sidebar styling.
* Only workflow items are interactive — everything else is pointer-events-none.
*
* Colors sourced from the dark theme CSS variables:
* --surface-1: #1e1e1e, --surface-5: #363636, --border: #2c2c2c, --border-1: #3d3d3d
* --text-primary: #e6e6e6, --text-tertiary: #b3b3b3, --text-muted: #787878
*/
export function LandingPreviewSidebar({
workflows,
activeWorkflowId,
onSelectWorkflow,
}: LandingPreviewSidebarProps) {
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
const handleToggle = useCallback(() => {
setIsDropdownOpen((prev) => !prev)
}, [])
useEffect(() => {
if (!isDropdownOpen) return
const handleClickOutside = (e: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
setIsDropdownOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [isDropdownOpen])
return (
<div className='flex h-full w-[220px] flex-shrink-0 flex-col border-[#2c2c2c] border-r bg-[#1e1e1e]'>
{/* Header */}
<div className='relative flex-shrink-0 px-[14px] pt-[12px]' ref={dropdownRef}>
<div className='flex items-center justify-between'>
<button
type='button'
onClick={handleToggle}
className='group -mx-[6px] flex cursor-pointer items-center gap-[8px] rounded-[6px] bg-transparent px-[6px] py-[4px] transition-colors hover:bg-[#363636]'
>
<span className='truncate font-base text-[#e6e6e6] text-[14px]'>My Workspace</span>
<ChevronDown
className={`h-[8px] w-[10px] flex-shrink-0 text-[#787878] transition-all duration-100 group-hover:text-[#cccccc] ${isDropdownOpen ? 'rotate-180' : ''}`}
/>
</button>
<div className='pointer-events-none flex flex-shrink-0 items-center'>
<Search className='h-[14px] w-[14px] text-[#787878]' />
</div>
</div>
{/* Workspace switcher dropdown */}
{isDropdownOpen && (
<div className='absolute top-[42px] left-[8px] z-50 min-w-[160px] max-w-[160px] rounded-[6px] bg-[#242424] px-[6px] py-[6px] shadow-lg'>
<div
className='flex h-[26px] cursor-pointer items-center gap-[8px] rounded-[6px] bg-[#3d3d3d] px-[6px] font-base text-[#e6e6e6] text-[13px]'
role='menuitem'
onClick={() => setIsDropdownOpen(false)}
>
<span className='min-w-0 flex-1 truncate'>My Workspace</span>
</div>
</div>
)}
</div>
{/* Workflow items */}
<div className='mt-[8px] space-y-[2px] overflow-x-hidden px-[8px]'>
{workflows.map((workflow) => {
const isActive = workflow.id === activeWorkflowId
return (
<button
key={workflow.id}
type='button'
onClick={() => onSelectWorkflow(workflow.id)}
className={`group flex h-[26px] w-full items-center gap-[8px] rounded-[8px] px-[6px] text-[14px] transition-colors ${
isActive ? 'bg-[#363636]' : 'bg-transparent hover:bg-[#363636]'
}`}
>
<div
className='h-[14px] w-[14px] flex-shrink-0 rounded-[4px]'
style={{ backgroundColor: workflow.color }}
/>
<div className='min-w-0 flex-1'>
<div
className={`min-w-0 truncate text-left font-medium ${
isActive ? 'text-[#e6e6e6]' : 'text-[#b3b3b3] group-hover:text-[#e6e6e6]'
}`}
>
{workflow.name}
</div>
</div>
</button>
)
})}
</div>
{/* Footer navigation — static */}
<div className='pointer-events-none mt-auto flex flex-shrink-0 flex-col gap-[2px] border-[#2c2c2c] border-t px-[7.75px] pt-[8px] pb-[8px]'>
{FOOTER_NAV_ITEMS.map((item) => {
const Icon = item.icon
return (
<div
key={item.id}
className='flex h-[26px] items-center gap-[8px] rounded-[8px] px-[6px] text-[14px]'
>
<Icon className='h-[14px] w-[14px] flex-shrink-0 text-[#b3b3b3]' />
<span className='truncate font-medium text-[#b3b3b3] text-[13px]'>{item.label}</span>
</div>
)
})}
</div>
</div>
)
}

View File

@@ -0,0 +1,162 @@
'use client'
import { useCallback, useMemo, useState } from 'react'
import { motion } from 'framer-motion'
import ReactFlow, {
applyEdgeChanges,
applyNodeChanges,
type Edge,
type EdgeProps,
type EdgeTypes,
getSmoothStepPath,
type Node,
type NodeTypes,
type OnEdgesChange,
type OnNodesChange,
ReactFlowProvider,
} from 'reactflow'
import 'reactflow/dist/style.css'
import { PreviewBlockNode } from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/preview-block-node'
import {
EASE_OUT,
type PreviewWorkflow,
toReactFlowElements,
} from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
interface FitViewOptions {
padding?: number
maxZoom?: number
}
interface LandingPreviewWorkflowProps {
workflow: PreviewWorkflow
animate?: boolean
fitViewOptions?: FitViewOptions
}
/**
* Custom edge that draws left-to-right on initial load via stroke animation.
* Falls back to a static path when `data.animate` is false.
*/
function PreviewEdge({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style,
data,
}: EdgeProps) {
const [edgePath] = getSmoothStepPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
})
if (data?.animate) {
return (
<motion.path
id={id}
className='react-flow__edge-path'
d={edgePath}
style={{ ...style, fill: 'none' }}
initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{
pathLength: { duration: 0.4, delay: data.delay ?? 0, ease: EASE_OUT },
opacity: { duration: 0.15, delay: data.delay ?? 0 },
}}
/>
)
}
return (
<path
id={id}
className='react-flow__edge-path'
d={edgePath}
style={{ ...style, fill: 'none' }}
/>
)
}
const NODE_TYPES: NodeTypes = { previewBlock: PreviewBlockNode }
const EDGE_TYPES: EdgeTypes = { previewEdge: PreviewEdge }
const PRO_OPTIONS = { hideAttribution: true }
const DEFAULT_FIT_VIEW_OPTIONS = { padding: 0.3, maxZoom: 1 } as const
/**
* Inner flow component. Keyed on workflow ID by the parent so it remounts
* cleanly on workflow switch — fitView fires on mount with zero delay.
*/
function PreviewFlow({ workflow, animate = false, fitViewOptions }: LandingPreviewWorkflowProps) {
const { nodes: initialNodes, edges: initialEdges } = useMemo(
() => toReactFlowElements(workflow, animate),
[workflow, animate]
)
const [nodes, setNodes] = useState<Node[]>(initialNodes)
const [edges, setEdges] = useState<Edge[]>(initialEdges)
const onNodesChange: OnNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[]
)
const onEdgesChange: OnEdgesChange = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[]
)
const resolvedFitViewOptions = fitViewOptions ?? DEFAULT_FIT_VIEW_OPTIONS
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={NODE_TYPES}
edgeTypes={EDGE_TYPES}
defaultEdgeOptions={{ type: 'previewEdge' }}
elementsSelectable={false}
nodesDraggable
nodesConnectable={false}
zoomOnScroll={false}
zoomOnDoubleClick={false}
panOnScroll={false}
zoomOnPinch={false}
panOnDrag
preventScrolling={false}
autoPanOnNodeDrag={false}
proOptions={PRO_OPTIONS}
fitView
fitViewOptions={resolvedFitViewOptions}
className='h-full w-full bg-[#1b1b1b]'
/>
)
}
/**
* Lightweight ReactFlow canvas displaying an interactive workflow preview.
* The key on workflow.id forces a clean remount on switch — instant fitView,
* no timers, no flicker.
*/
export function LandingPreviewWorkflow({
workflow,
animate = false,
fitViewOptions,
}: LandingPreviewWorkflowProps) {
return (
<div className='h-full w-full'>
<ReactFlowProvider key={workflow.id}>
<PreviewFlow workflow={workflow} animate={animate} fitViewOptions={fitViewOptions} />
</ReactFlowProvider>
</div>
)
}

View File

@@ -0,0 +1,307 @@
'use client'
import { memo } from 'react'
import { motion } from 'framer-motion'
import { Database } from 'lucide-react'
import { Handle, type NodeProps, Position } from 'reactflow'
import {
AgentIcon,
AnthropicIcon,
FirecrawlIcon,
GeminiIcon,
GithubIcon,
GmailIcon,
GoogleCalendarIcon,
GoogleSheetsIcon,
JiraIcon,
LinearIcon,
LinkedInIcon,
MistralIcon,
NotionIcon,
OpenAIIcon,
RedditIcon,
ReductoIcon,
ScheduleIcon,
SlackIcon,
StartIcon,
SupabaseIcon,
TelegramIcon,
TextractIcon,
WebhookIcon,
xAIIcon,
xIcon,
YouTubeIcon,
} from '@/components/icons'
import {
BLOCK_STAGGER,
EASE_OUT,
type PreviewTool,
} from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
/** Map block type strings to their icon components. */
const BLOCK_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
starter: StartIcon,
start_trigger: StartIcon,
agent: AgentIcon,
slack: SlackIcon,
jira: JiraIcon,
x: xIcon,
youtube: YouTubeIcon,
schedule: ScheduleIcon,
telegram: TelegramIcon,
knowledge_base: Database,
webhook: WebhookIcon,
github: GithubIcon,
supabase: SupabaseIcon,
google_calendar: GoogleCalendarIcon,
gmail: GmailIcon,
google_sheets: GoogleSheetsIcon,
linear: LinearIcon,
firecrawl: FirecrawlIcon,
reddit: RedditIcon,
notion: NotionIcon,
reducto: ReductoIcon,
textract: TextractIcon,
linkedin: LinkedInIcon,
}
/** Model prefix → provider icon for the "Model" row in agent blocks. */
const MODEL_PROVIDER_ICONS: Array<{
prefix: string
icon: React.ComponentType<{ className?: string }>
size?: string
}> = [
{ prefix: 'gpt-', icon: OpenAIIcon },
{ prefix: 'o3', icon: OpenAIIcon },
{ prefix: 'o4', icon: OpenAIIcon },
{ prefix: 'claude-', icon: AnthropicIcon },
{ prefix: 'gemini-', icon: GeminiIcon },
{ prefix: 'grok-', icon: xAIIcon, size: 'h-[17px] w-[17px]' },
{ prefix: 'mistral-', icon: MistralIcon },
]
function getModelIconEntry(modelValue: string) {
const lower = modelValue.toLowerCase()
return MODEL_PROVIDER_ICONS.find((m) => lower.startsWith(m.prefix)) ?? null
}
/**
* Data shape for preview block nodes
*/
interface PreviewBlockData {
name: string
blockType: string
bgColor: string
rows: Array<{ title: string; value: string }>
tools?: PreviewTool[]
markdown?: string
hideTargetHandle?: boolean
hideSourceHandle?: boolean
index?: number
animate?: boolean
}
/**
* Handle styling matching the real WorkflowBlock handles.
* --workflow-edge in dark mode: #454545
*/
const HANDLE_BASE = '!z-[10] !border-none !bg-[#454545]'
const HANDLE_LEFT = `${HANDLE_BASE} !left-[-8px] !h-5 !w-[7px] !rounded-r-none !rounded-l-[2px]`
const HANDLE_RIGHT = `${HANDLE_BASE} !right-[-8px] !h-5 !w-[7px] !rounded-l-none !rounded-r-[2px]`
/**
* Static preview block node matching the real WorkflowBlock styling.
* Renders a block header with icon + name, sub-block rows, and tool chips.
*
* Colors sourced from dark theme CSS variables:
* --surface-2: #232323, --border-1: #3d3d3d
* --text-primary: #e6e6e6, --text-tertiary: #b3b3b3
*/
export const PreviewBlockNode = memo(function PreviewBlockNode({
data,
}: NodeProps<PreviewBlockData>) {
const {
name,
blockType,
bgColor,
rows,
tools,
markdown,
hideTargetHandle,
hideSourceHandle,
index = 0,
animate = false,
} = data
const Icon = BLOCK_ICONS[blockType]
const delay = animate ? index * BLOCK_STAGGER : 0
if (blockType === 'note' && markdown) {
return (
<motion.div
className='relative'
initial={animate ? { opacity: 0 } : false}
animate={{ opacity: 1 }}
transition={{ duration: 0.45, delay, ease: EASE_OUT }}
>
<div className='w-[280px] select-none rounded-[8px] border border-[#3d3d3d] bg-[#232323]'>
<div className='border-[#3d3d3d] border-b p-[8px]'>
<span className='font-medium text-[#e6e6e6] text-[16px]'>Note</span>
</div>
<div className='p-[10px]'>
<NoteMarkdown content={markdown} />
</div>
</div>
</motion.div>
)
}
const hasContent = rows.length > 0 || (tools && tools.length > 0)
return (
<motion.div
className='relative'
initial={animate ? { opacity: 0 } : false}
animate={{ opacity: 1 }}
transition={{ duration: 0.45, delay, ease: EASE_OUT }}
>
<div className='relative z-[20] w-[250px] select-none rounded-[8px] border border-[#3d3d3d] bg-[#232323]'>
{/* Target handle (left side) */}
{!hideTargetHandle && (
<Handle
type='target'
position={Position.Left}
id='target'
className={HANDLE_LEFT}
style={{ top: '20px', transform: 'translateY(-50%)' }}
isConnectableStart={false}
isConnectableEnd={false}
/>
)}
{/* Header */}
<div
className={`flex items-center justify-between p-[8px] ${hasContent ? 'border-[#3d3d3d] border-b' : ''}`}
>
<div className='relative z-10 flex min-w-0 flex-1 items-center gap-[10px]'>
<div
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
style={{ background: bgColor }}
>
{Icon && <Icon className='h-[16px] w-[16px] text-white' />}
</div>
<span className='truncate font-medium text-[#e6e6e6] text-[16px]'>{name}</span>
</div>
</div>
{/* Sub-block rows + tools */}
{hasContent && (
<div className='flex flex-col gap-[8px] p-[8px]'>
{rows.map((row) => {
const modelEntry = row.title === 'Model' ? getModelIconEntry(row.value) : null
const ModelIcon = modelEntry?.icon
return (
<div key={row.title} className='flex items-center gap-[8px]'>
<span className='flex-shrink-0 font-normal text-[#b3b3b3] text-[14px] capitalize'>
{row.title}
</span>
{row.value && (
<span className='flex min-w-0 flex-1 items-center justify-end gap-[5px] font-normal text-[#e6e6e6] text-[14px]'>
{ModelIcon && (
<ModelIcon
className={`inline-block flex-shrink-0 text-[#e6e6e6] ${modelEntry.size ?? 'h-[14px] w-[14px]'}`}
/>
)}
<span className='truncate'>{row.value}</span>
</span>
)}
</div>
)
})}
{/* Tool chips — inline with label */}
{tools && tools.length > 0 && (
<div className='flex items-center gap-[8px]'>
<span className='flex-shrink-0 font-normal text-[#b3b3b3] text-[14px]'>Tools</span>
<div className='flex flex-1 flex-wrap items-center justify-end gap-[5px]'>
{tools.map((tool) => {
const ToolIcon = BLOCK_ICONS[tool.type]
return (
<div
key={tool.type}
className='flex items-center gap-[5px] rounded-[5px] border border-[#3d3d3d] bg-[#2a2a2a] px-[6px] py-[3px]'
>
<div
className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-[4px]'
style={{ background: tool.bgColor }}
>
{ToolIcon && <ToolIcon className='h-[10px] w-[10px] text-white' />}
</div>
<span className='font-normal text-[#e6e6e6] text-[12px]'>{tool.name}</span>
</div>
)
})}
</div>
</div>
)}
</div>
)}
{/* Source handle (right side) */}
{!hideSourceHandle && (
<Handle
type='source'
position={Position.Right}
id='source'
className={HANDLE_RIGHT}
style={{ top: '20px', transform: 'translateY(-50%)' }}
isConnectableStart={false}
isConnectableEnd={false}
/>
)}
</div>
</motion.div>
)
})
/**
* Renders lightweight markdown-like content for note blocks.
* Supports ### headings, **bold**, _italic_, --- rules, and blank-line spacing.
*/
function NoteMarkdown({ content }: { content: string }) {
const lines = content.split('\n')
return (
<div className='flex flex-col gap-[4px]'>
{lines.map((line, i) => {
const trimmed = line.trim()
if (!trimmed) return <div key={i} className='h-[4px]' />
if (trimmed === '---') {
return <hr key={i} className='my-[4px] border-[#3d3d3d] border-t' />
}
if (trimmed.startsWith('### ')) {
return (
<p key={i} className='font-semibold text-[#e6e6e6] text-[16px] leading-[1.3]'>
{trimmed.slice(4)}
</p>
)
}
return (
<p
key={i}
className='font-medium text-[#e6e6e6] text-[13px] leading-[1.5]'
dangerouslySetInnerHTML={{
__html: trimmed
.replace(/\*\*_(.+?)_\*\*/g, '<strong><em>$1</em></strong>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/_"(.+?)"_/g, '<em>&ldquo;$1&rdquo;</em>')
.replace(/_(.+?)_/g, '<em>$1</em>'),
}}
/>
)
})}
</div>
)
}

View File

@@ -0,0 +1,228 @@
import type { Edge, Node } from 'reactflow'
import { Position } from 'reactflow'
/**
* Tool entry displayed as a chip on agent blocks
*/
export interface PreviewTool {
name: string
type: string
bgColor: string
}
/**
* Static block definition for preview workflow nodes
*/
export interface PreviewBlock {
id: string
name: string
type: string
bgColor: string
rows: Array<{ title: string; value: string }>
tools?: PreviewTool[]
markdown?: string
position: { x: number; y: number }
hideTargetHandle?: boolean
hideSourceHandle?: boolean
}
/**
* Workflow definition containing nodes, edges, and metadata
*/
export interface PreviewWorkflow {
id: string
name: string
color: string
blocks: PreviewBlock[]
edges: Array<{ id: string; source: string; target: string }>
/** Public JSON export used to seed the landing-page import flow */
seedPath?: string
}
/**
* IT Service Management workflow — Slack Trigger -> Agent (KB tool) -> Jira
*/
const IT_SERVICE_WORKFLOW: PreviewWorkflow = {
id: 'wf-it-service',
name: 'IT Service Management',
color: '#FF6B2C',
blocks: [
{
id: 'slack-1',
name: 'Slack',
type: 'slack',
bgColor: '#611f69',
rows: [
{ title: 'Channel', value: '#it-support' },
{ title: 'Event', value: 'New Message' },
],
position: { x: 80, y: 140 },
hideTargetHandle: true,
},
{
id: 'agent-1',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'claude-sonnet-4.6' },
{ title: 'System Prompt', value: 'Triage incoming IT...' },
],
tools: [{ name: 'Knowledge Base', type: 'knowledge_base', bgColor: '#10B981' }],
position: { x: 420, y: 40 },
},
{
id: 'jira-1',
name: 'Jira',
type: 'jira',
bgColor: '#E0E0E0',
rows: [
{ title: 'Operation', value: 'Get Issues' },
{ title: 'Project', value: 'IT-Support' },
],
position: { x: 420, y: 260 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'slack-1', target: 'agent-1' },
{ id: 'e-2', source: 'slack-1', target: 'jira-1' },
],
}
/**
* Content pipeline workflow — Schedule -> Agent (X + YouTube tools)
*/
const CONTENT_PIPELINE_WORKFLOW: PreviewWorkflow = {
id: 'wf-content-pipeline',
name: 'Content Pipeline',
color: '#33C482',
blocks: [
{
id: 'schedule-1',
name: 'Schedule',
type: 'schedule',
bgColor: '#6366F1',
rows: [
{ title: 'Run Frequency', value: 'Daily' },
{ title: 'Time', value: '09:00 AM' },
],
position: { x: 80, y: 140 },
hideTargetHandle: true,
},
{
id: 'agent-2',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'grok-4' },
{ title: 'System Prompt', value: 'Repurpose trending...' },
],
tools: [
{ name: 'X', type: 'x', bgColor: '#000000' },
{ name: 'YouTube', type: 'youtube', bgColor: '#FF0000' },
],
position: { x: 420, y: 180 },
hideSourceHandle: true,
},
],
edges: [{ id: 'e-3', source: 'schedule-1', target: 'agent-2' }],
}
/**
* Empty "New Agent" workflow — a single note prompting the user to start building
*/
const NEW_AGENT_WORKFLOW: PreviewWorkflow = {
id: 'wf-new-agent',
name: 'New Agent',
color: '#787878',
blocks: [
{
id: 'note-1',
name: '',
type: 'note',
bgColor: 'transparent',
rows: [],
markdown: '### What will you build?\n\n_"Find Linear todos and send in Slack"_',
position: { x: 0, y: 0 },
hideTargetHandle: true,
hideSourceHandle: true,
},
],
edges: [],
}
export const PREVIEW_WORKFLOWS: PreviewWorkflow[] = [
CONTENT_PIPELINE_WORKFLOW,
IT_SERVICE_WORKFLOW,
NEW_AGENT_WORKFLOW,
]
/** Stagger delay between each block appearing (seconds). */
export const BLOCK_STAGGER = 0.12
/** Shared cubic-bezier easing — fast deceleration, gentle settle. */
export const EASE_OUT: [number, number, number, number] = [0.16, 1, 0.3, 1]
/** Shared edge style applied to all preview workflow connections */
const EDGE_STYLE = { stroke: '#454545', strokeWidth: 1.5 } as const
/**
* Converts a PreviewWorkflow to React Flow nodes and edges.
*
* @param workflow - The workflow definition
* @param animate - When true, node/edge data includes animation metadata
*/
export function toReactFlowElements(
workflow: PreviewWorkflow,
animate = false
): {
nodes: Node[]
edges: Edge[]
} {
const blockIndexMap = new Map(workflow.blocks.map((b, i) => [b.id, i]))
const nodes: Node[] = workflow.blocks.map((block, index) => ({
id: block.id,
type: 'previewBlock',
position: block.position,
data: {
name: block.name,
blockType: block.type,
bgColor: block.bgColor,
rows: block.rows,
tools: block.tools,
markdown: block.markdown,
hideTargetHandle: block.hideTargetHandle,
hideSourceHandle: block.hideSourceHandle,
index,
animate,
},
draggable: true,
selectable: false,
connectable: false,
sourcePosition: Position.Right,
targetPosition: Position.Left,
}))
const edges: Edge[] = workflow.edges.map((e) => {
const sourceIndex = blockIndexMap.get(e.source) ?? 0
return {
id: e.id,
source: e.source,
target: e.target,
type: 'previewEdge',
animated: false,
style: EDGE_STYLE,
sourceHandle: 'source',
targetHandle: 'target',
data: {
animate,
delay: animate ? sourceIndex * BLOCK_STAGGER + BLOCK_STAGGER : 0,
},
}
})
return { nodes, edges }
}

View File

@@ -0,0 +1,91 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { motion, type Variants } from 'framer-motion'
import { LandingPreviewPanel } from '@/app/(home)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
import { LandingPreviewSidebar } from '@/app/(home)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar'
import { LandingPreviewWorkflow } from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/landing-preview-workflow'
import {
EASE_OUT,
PREVIEW_WORKFLOWS,
} from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
const containerVariants: Variants = {
hidden: {},
visible: {
transition: { staggerChildren: 0.15 },
},
}
const sidebarVariants: Variants = {
hidden: { opacity: 0, x: -12 },
visible: {
opacity: 1,
x: 0,
transition: {
x: { duration: 0.25, ease: EASE_OUT },
opacity: { duration: 0.25, ease: EASE_OUT },
},
},
}
const panelVariants: Variants = {
hidden: { opacity: 0, x: 12 },
visible: {
opacity: 1,
x: 0,
transition: {
x: { duration: 0.25, ease: EASE_OUT },
opacity: { duration: 0.25, ease: EASE_OUT },
},
},
}
/**
* Interactive workspace preview for the hero section.
*
* Renders a lightweight replica of the Sim workspace with:
* - A sidebar with two selectable workflows
* - A ReactFlow canvas showing the active workflow's blocks and edges
* - A panel with a functional copilot input (stores prompt + redirects to /signup)
*
* Everything except the workflow items and the copilot input is non-interactive.
* On mount the sidebar slides from left and the panel from right. The canvas
* background stays fully opaque; individual block nodes animate in with a
* staggered fade. Edges draw left-to-right. Animations only fire on initial
* load — workflow switches render instantly.
*/
export function LandingPreview() {
const [activeWorkflowId, setActiveWorkflowId] = useState(PREVIEW_WORKFLOWS[0].id)
const isInitialMount = useRef(true)
useEffect(() => {
isInitialMount.current = false
}, [])
const activeWorkflow =
PREVIEW_WORKFLOWS.find((w) => w.id === activeWorkflowId) ?? PREVIEW_WORKFLOWS[0]
return (
<motion.div
className='dark flex aspect-[1116/549] w-full overflow-hidden rounded bg-[#1b1b1b] antialiased'
initial='hidden'
animate='visible'
variants={containerVariants}
>
<motion.div className='hidden lg:flex' variants={sidebarVariants}>
<LandingPreviewSidebar
workflows={PREVIEW_WORKFLOWS}
activeWorkflowId={activeWorkflowId}
onSelectWorkflow={setActiveWorkflowId}
/>
</motion.div>
<div className='relative flex-1 overflow-hidden'>
<LandingPreviewWorkflow workflow={activeWorkflow} animate={isInitialMount.current} />
</div>
<motion.div className='hidden lg:flex' variants={panelVariants}>
<LandingPreviewPanel />
</motion.div>
</motion.div>
)
}

View File

@@ -0,0 +1,41 @@
'use client'
import { useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
import { GithubOutlineIcon } from '@/components/icons'
import { getFormattedGitHubStars } from '@/app/(landing)/actions/github'
const logger = createLogger('github-stars')
const INITIAL_STARS = '26.4k'
/**
* Client component that displays GitHub stars count.
*
* Isolated as a client component to allow the parent Navbar to remain
* a Server Component for optimal SEO/GEO crawlability.
*/
export function GitHubStars() {
const [stars, setStars] = useState(INITIAL_STARS)
useEffect(() => {
getFormattedGitHubStars()
.then(setStars)
.catch((error) => {
logger.warn('Failed to fetch GitHub stars', error)
})
}, [])
return (
<a
href='https://github.com/simstudioai/sim'
target='_blank'
rel='noopener noreferrer'
className='flex items-center gap-[8px] px-[12px]'
aria-label={`GitHub repository — ${stars} stars`}
>
<GithubOutlineIcon className='h-[14px] w-[14px]' />
<span aria-live='polite'>{stars}</span>
</a>
)
}

View File

@@ -0,0 +1,97 @@
import Image from 'next/image'
import Link from 'next/link'
import { ChevronDown } from '@/components/emcn'
import { GitHubStars } from '@/app/(home)/components/navbar/components/github-stars'
interface NavLink {
label: string
href: string
external?: boolean
icon?: 'chevron'
}
const NAV_LINKS: NavLink[] = [
{ label: 'Docs', href: '/docs', icon: 'chevron' },
{ label: 'Pricing', href: '/pricing' },
{ label: 'Careers', href: '/careers' },
{ label: 'Enterprise', href: '/enterprise' },
]
/** Logo and nav edge: horizontal padding (px) for left/right symmetry. */
const LOGO_CELL = 'flex items-center px-[20px]'
/** Links: even spacing between items. */
const LINK_CELL = 'flex items-center px-[14px]'
export default function Navbar() {
return (
<nav
aria-label='Primary navigation'
className='flex h-[52px] border-[#2A2A2A] border-b-[1px] bg-[#1C1C1C] font-[430] font-season text-[#ECECEC] text-[14px]'
itemScope
itemType='https://schema.org/SiteNavigationElement'
>
{/* Logo */}
<Link href='/' className={LOGO_CELL} aria-label='Sim home' itemProp='url'>
<span itemProp='name' className='sr-only'>
Sim
</span>
<Image
src='/logo/sim-landing.svg'
alt='Sim'
width={71}
height={22}
className='h-[22px] w-auto'
priority
/>
</Link>
{/* Links */}
<ul className='mt-[0.75px] flex'>
{NAV_LINKS.map(({ label, href, external, icon }) => (
<li key={label} className='flex'>
{external ? (
<a href={href} target='_blank' rel='noopener noreferrer' className={LINK_CELL}>
{label}
</a>
) : (
<Link
href={href}
className={icon ? `${LINK_CELL} gap-[8px]` : LINK_CELL}
aria-label={label}
>
{label}
{icon === 'chevron' && (
<ChevronDown className='mt-[1.75px] h-[10px] w-[10px] flex-shrink-0 text-[#ECECEC]' />
)}
</Link>
)}
</li>
))}
<li className='flex'>
<GitHubStars />
</li>
</ul>
<div className='flex-1' />
{/* CTAs */}
<div className='flex items-center gap-[8px] px-[20px]'>
<Link
href='/login'
className='inline-flex h-[30px] items-center rounded-[5px] border border-[#3d3d3d] px-[9px] text-[#ECECEC] text-[13.5px] transition-colors hover:bg-[#2A2A2A]'
aria-label='Log in'
>
Log in
</Link>
<Link
href='/signup'
className='inline-flex h-[30px] items-center gap-[7px] rounded-[5px] border border-[#33C482] bg-[#33C482] px-[9px] text-[13.5px] text-black transition-[filter] hover:brightness-110'
aria-label='Get started with Sim'
>
Get started
</Link>
</div>
</nav>
)
}

View File

@@ -0,0 +1,204 @@
import Link from 'next/link'
import { Badge } from '@/components/emcn'
interface PricingTier {
id: string
name: string
description: string
price: string
billingPeriod?: string
color: string
features: string[]
cta: { label: string; href: string }
}
const PRICING_TIERS: PricingTier[] = [
{
id: 'community',
name: 'Community',
description: 'For individuals getting started with AI agents',
price: 'Free',
color: '#2ABBF8',
features: [
'3,000 credits/mo',
'5GB file storage',
'5 min execution limit',
'Limited log retention',
'CLI/SDK Access',
],
cta: { label: 'Get started', href: '/signup' },
},
{
id: 'pro',
name: 'Pro',
description: 'For professionals building production workflows',
price: '$25',
billingPeriod: 'per month',
color: '#00F701',
features: [
'6,000 credits/mo',
'+50 daily refresh credits',
'150 runs/min (sync)',
'50 min sync execution limit',
'50GB file storage',
],
cta: { label: 'Get started', href: '/signup' },
},
{
id: 'max',
name: 'Max',
description: 'For power users and teams building at scale',
price: '$100',
billingPeriod: 'per month',
color: '#FA4EDF',
features: [
'25,000 credits/mo',
'+200 daily refresh credits',
'300 runs/min (sync)',
'50 min sync execution limit',
'500GB file storage',
],
cta: { label: 'Get started', href: '/signup' },
},
{
id: 'enterprise',
name: 'Enterprise',
description: 'For organizations needing security and scale',
price: 'Custom',
color: '#FFCC02',
features: ['Custom infra limits', 'SSO', 'SOC2', 'Self hosting', 'Dedicated support'],
cta: { label: 'Book a demo', href: '/contact' },
},
]
function CheckIcon({ color }: { color: string }) {
return (
<svg width='14' height='14' viewBox='0 0 14 14' fill='none'>
<path
d='M2.5 7L5.5 10L11.5 4'
stroke={color}
strokeWidth='1.5'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
)
}
interface PricingCardProps {
tier: PricingTier
}
function PricingCard({ tier }: PricingCardProps) {
const isEnterprise = tier.id === 'enterprise'
const isPro = tier.id === 'pro'
return (
<article className='flex flex-1 flex-col' aria-labelledby={`${tier.id}-heading`}>
<div className='flex flex-1 flex-col gap-6 rounded-t-lg border border-[#E5E5E5] border-b-0 bg-white p-5'>
<div className='flex flex-col'>
<h3
id={`${tier.id}-heading`}
className='font-[430] font-season text-[#1C1C1C] text-[24px] leading-[100%] tracking-[-0.02em]'
>
{tier.name}
</h3>
<p className='mt-2 min-h-[44px] font-[430] font-season text-[#5c5c5c] text-[14px] leading-[125%] tracking-[0.02em]'>
{tier.description}
</p>
<p className='mt-4 flex items-center gap-1.5 font-[430] font-season text-[#1C1C1C] text-[20px] leading-[100%] tracking-[-0.02em]'>
{tier.price}
{tier.billingPeriod && (
<span className='text-[#737373] text-[16px]'>{tier.billingPeriod}</span>
)}
</p>
<div className='mt-4'>
{isEnterprise ? (
<a
href={tier.cta.href}
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#E5E5E5] px-[10px] font-[430] font-season text-[#1C1C1C] text-[14px] transition-colors hover:bg-[#F0F0F0]'
>
{tier.cta.label}
</a>
) : isPro ? (
<Link
href={tier.cta.href}
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#33C482] bg-[#33C482] px-[10px] font-[430] font-season text-[14px] text-white transition-[filter] hover:brightness-110'
>
{tier.cta.label}
</Link>
) : (
<Link
href={tier.cta.href}
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#E5E5E5] px-[10px] font-[430] font-season text-[#1C1C1C] text-[14px] transition-colors hover:bg-[#F0F0F0]'
>
{tier.cta.label}
</Link>
)}
</div>
</div>
<ul className='flex flex-col gap-2'>
{tier.features.map((feature) => (
<li key={feature} className='flex items-center gap-2'>
<CheckIcon color='#404040' />
<span className='font-[400] font-season text-[#5c5c5c] text-[14px] leading-[125%] tracking-[0.02em]'>
{feature}
</span>
</li>
))}
</ul>
</div>
<div className='relative h-[6px]'>
<div
className='absolute inset-0 rounded-b-sm opacity-60'
style={{ backgroundColor: tier.color }}
/>
<div
className='absolute top-0 right-0 bottom-0 left-[12%] rounded-b-sm opacity-60'
style={{ backgroundColor: tier.color }}
/>
<div
className='absolute top-0 right-0 bottom-0 left-[25%] rounded-b-sm'
style={{ backgroundColor: tier.color }}
/>
</div>
</article>
)
}
/**
* Pricing section -- tiered pricing plans with feature comparison.
*/
export default function Pricing() {
return (
<section id='pricing' aria-labelledby='pricing-heading' className='bg-[#F6F6F6]'>
<div className='px-4 pt-[100px] pb-8 sm:px-8 md:px-[80px]'>
<div className='flex flex-col items-start gap-3 sm:gap-4 md:gap-[20px]'>
<Badge
variant='blue'
size='md'
dot
className='bg-[#2ABBF8]/10 font-season text-[#2ABBF8] uppercase tracking-[0.02em]'
>
Pricing
</Badge>
<h2
id='pricing-heading'
className='font-[430] font-season text-[#1C1C1C] text-[32px] leading-[100%] tracking-[-0.02em] sm:text-[36px] md:text-[40px]'
>
Pricing
</h2>
</div>
<div className='mt-12 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4'>
{PRICING_TIERS.map((tier) => (
<PricingCard key={tier.id} tier={tier} />
))}
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,224 @@
/**
* JSON-LD structured data for the landing page.
*
* Renders a `<script type="application/ld+json">` with Schema.org markup.
* Single source of truth for machine-readable page metadata.
*
* Schemas: Organization, WebSite, WebPage, BreadcrumbList, WebApplication, FAQPage.
*
* AI crawler behavior (2025-2026):
* - Google AI Overviews / Bing Copilot parse JSON-LD from their search indexes.
* - GPTBot indexes JSON-LD during crawling (92% of LLM crawlers parse JSON-LD first).
* - Perplexity / Claude prioritize visible HTML over JSON-LD during direct fetch.
* - All claims here must also appear as visible text on the page.
*
* Maintenance:
* - Offer prices must match the Pricing component exactly.
* - `sameAs` links must match the Footer social links.
* - Do not add `aggregateRating` without real, verifiable review data.
*/
export default function StructuredData() {
const structuredData = {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'Organization',
'@id': 'https://sim.ai/#organization',
name: 'Sim',
alternateName: 'Sim Studio',
description:
'Sim is 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.',
url: 'https://sim.ai',
logo: {
'@type': 'ImageObject',
'@id': 'https://sim.ai/#logo',
url: 'https://sim.ai/logo/b%26w/text/b%26w.svg',
contentUrl: 'https://sim.ai/logo/b%26w/text/b%26w.svg',
width: 49.78314,
height: 24.276,
caption: 'Sim Logo',
},
image: { '@id': 'https://sim.ai/#logo' },
sameAs: [
'https://x.com/simdotai',
'https://github.com/simstudioai/sim',
'https://www.linkedin.com/company/simstudioai/',
'https://discord.gg/Hr4UWYEcTT',
],
contactPoint: {
'@type': 'ContactPoint',
contactType: 'customer support',
availableLanguage: ['en'],
},
},
{
'@type': 'WebSite',
'@id': 'https://sim.ai/#website',
url: 'https://sim.ai',
name: 'Sim — Build AI Agents & Run Your Agentic Workforce',
description:
'Sim is 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. Join 100,000+ builders.',
publisher: { '@id': 'https://sim.ai/#organization' },
inLanguage: 'en-US',
},
{
'@type': 'WebPage',
'@id': 'https://sim.ai/#webpage',
url: 'https://sim.ai',
name: 'Sim — Build AI Agents & Run Your Agentic Workforce',
isPartOf: { '@id': 'https://sim.ai/#website' },
about: { '@id': 'https://sim.ai/#software' },
datePublished: '2024-01-01T00:00:00+00:00',
dateModified: new Date().toISOString(),
description:
'Sim is 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. Create agents, workflows, knowledge bases, tables, and docs.',
breadcrumb: { '@id': 'https://sim.ai/#breadcrumb' },
inLanguage: 'en-US',
potentialAction: [{ '@type': 'ReadAction', target: ['https://sim.ai'] }],
},
{
'@type': 'BreadcrumbList',
'@id': 'https://sim.ai/#breadcrumb',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Home', item: 'https://sim.ai' },
],
},
{
'@type': 'WebApplication',
'@id': 'https://sim.ai/#software',
url: 'https://sim.ai',
name: 'Sim — Build AI Agents & Run Your Agentic Workforce',
description:
'Sim is 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. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders. SOC2 and HIPAA compliant.',
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Web',
browserRequirements: 'Requires a modern browser with JavaScript enabled',
offers: [
{
'@type': 'Offer',
name: 'Community Plan — 3,000 credits included',
price: '0',
priceCurrency: 'USD',
availability: 'https://schema.org/InStock',
},
{
'@type': 'Offer',
name: 'Pro Plan — 6,000 credits/month',
price: '25',
priceCurrency: 'USD',
priceSpecification: {
'@type': 'UnitPriceSpecification',
price: '25',
priceCurrency: 'USD',
unitText: 'MONTH',
billingIncrement: 1,
},
availability: 'https://schema.org/InStock',
},
{
'@type': 'Offer',
name: 'Max Plan — 25,000 credits/month',
price: '100',
priceCurrency: 'USD',
priceSpecification: {
'@type': 'UnitPriceSpecification',
price: '100',
priceCurrency: 'USD',
unitText: 'MONTH',
billingIncrement: 1,
},
availability: 'https://schema.org/InStock',
},
],
featureList: [
'AI agent creation',
'Agentic workflow orchestration',
'1,000+ integrations',
'LLM orchestration (OpenAI, Anthropic, Google, xAI, Mistral, Perplexity)',
'Knowledge base creation',
'Table creation',
'Document creation',
'API access',
'Custom functions',
'Scheduled workflows',
'Event triggers',
],
review: [
{
'@type': 'Review',
author: { '@type': 'Person', name: 'Hasan Toor' },
reviewBody:
'This startup just dropped the fastest way to build AI agents. This Figma-like canvas to build agents will blow your mind.',
url: 'https://x.com/hasantoxr/status/1912909502036525271',
},
{
'@type': 'Review',
author: { '@type': 'Person', name: 'nizzy' },
reviewBody:
'This is the zapier of agent building. I always believed that building agents and using AI should not be limited to technical people. I think this solves just that.',
url: 'https://x.com/nizzyabi/status/1907864421227180368',
},
{
'@type': 'Review',
author: { '@type': 'Organization', name: 'xyflow' },
reviewBody: 'A very good looking agent workflow builder and open source!',
url: 'https://x.com/xyflowdev/status/1909501499719438670',
},
],
},
{
'@type': 'FAQPage',
'@id': 'https://sim.ai/#faq',
mainEntity: [
{
'@type': 'Question',
name: 'What is Sim?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Sim is the open-source platform to build AI agents and run your agentic workforce. Teams connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders. SOC2 and HIPAA compliant.',
},
},
{
'@type': 'Question',
name: 'Which AI models does Sim support?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Sim supports all major AI models including OpenAI (GPT-5, GPT-4o), Anthropic (Claude), Google (Gemini), xAI (Grok), Mistral, Perplexity, and many more. You can also connect to open-source models via Ollama.',
},
},
{
'@type': 'Question',
name: 'How much does Sim cost?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Sim offers a free Community plan with 3,000 credits, a Pro plan at $25/month with 6,000 credits, a Max plan at $100/month with 25,000 credits, team plans available for both tiers, and custom Enterprise pricing. All plans include CLI/SDK access.',
},
},
{
'@type': 'Question',
name: 'Do I need coding skills to use Sim?',
acceptedAnswer: {
'@type': 'Answer',
text: 'No coding skills are required. Sim provides a visual interface for building AI agents and agentic workflows. Developers can also use custom functions, the API, and the CLI/SDK for advanced use cases.',
},
},
{
'@type': 'Question',
name: 'What enterprise features does Sim offer?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Sim offers SOC2 and HIPAA compliance, SSO/SAML authentication, role-based access control, audit logs, dedicated support, custom SLAs, and on-premise deployment options for enterprise customers.',
},
},
],
},
],
}
return (
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
/>
)
}

View File

@@ -0,0 +1,595 @@
import type { PreviewWorkflow } from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
/**
* OCR Invoice to DB — Start → Agent (Textract) → Supabase
* Pattern: Straight line (all blocks aligned at top)
*/
const OCR_INVOICE_WORKFLOW: PreviewWorkflow = {
id: 'tpl-ocr-invoice',
name: 'OCR Invoice to DB',
color: '#2ABBF8',
seedPath: '/landing-page-templates/ocr-invoice-db-d502887e-8750-40a3-a98b-fb2a4e725a4d.json',
blocks: [
{
id: 'starter-1',
name: 'Start',
type: 'starter',
bgColor: '#34B5FF',
rows: [{ title: 'URL', value: 'invoice.pdf' }],
position: { x: 40, y: 80 },
hideTargetHandle: true,
},
{
id: 'agent-1',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'gpt-5.2' },
{ title: 'System Prompt', value: 'Extract invoice fields...' },
],
tools: [{ name: 'Textract', type: 'textract', bgColor: '#055F4E' }],
position: { x: 400, y: 100 },
},
{
id: 'supabase-1',
name: 'Supabase',
type: 'supabase',
bgColor: '#1C1C1C',
rows: [
{ title: 'Table', value: 'invoices' },
{ title: 'Operation', value: 'Insert Row' },
],
position: { x: 760, y: 80 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'starter-1', target: 'agent-1' },
{ id: 'e-2', source: 'agent-1', target: 'supabase-1' },
],
}
/**
* GitHub Release Agent — GitHub → Agent → Slack
* Pattern: Convex (low → high → low)
*/
const GITHUB_RELEASE_WORKFLOW: PreviewWorkflow = {
id: 'tpl-github-release',
name: 'GitHub Release Agent',
color: '#00F701',
seedPath: '/landing-page-templates/gh-release-agent-d3bed10e-fc87-4fdb-b458-80d8e43757d3.json',
blocks: [
{
id: 'github-1',
name: 'GitHub',
type: 'github',
bgColor: '#181C1E',
rows: [
{ title: 'Event', value: 'New Release' },
{ title: 'Repository', value: 'org/repo' },
],
position: { x: 60, y: 140 },
hideTargetHandle: true,
},
{
id: 'agent-2',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'claude-sonnet-4.6' },
{ title: 'System Prompt', value: 'Summarize changelog...' },
],
position: { x: 370, y: 50 },
},
{
id: 'slack-1',
name: 'Slack',
type: 'slack',
bgColor: '#611f69',
rows: [
{ title: 'Channel', value: '#releases' },
{ title: 'Operation', value: 'Send Message' },
],
position: { x: 680, y: 140 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'github-1', target: 'agent-2' },
{ id: 'e-2', source: 'agent-2', target: 'slack-1' },
],
}
/**
* Meeting Follow-up Agent — Google Calendar → Agent → Gmail
* Pattern: Concave (high → low → high)
*/
const MEETING_FOLLOWUP_WORKFLOW: PreviewWorkflow = {
id: 'tpl-meeting-followup',
name: 'Meeting Follow-up Agent',
color: '#FFCC02',
seedPath:
'/landing-page-templates/meeting-followup-agent-a2357a2f-67f7-40c1-8e64-301bcd604239.json',
blocks: [
{
id: 'gcal-1',
name: 'Google Calendar',
type: 'google_calendar',
bgColor: '#E0E0E0',
rows: [
{ title: 'Event', value: 'Meeting Ended' },
{ title: 'Calendar', value: 'Work' },
],
position: { x: 60, y: 60 },
hideTargetHandle: true,
},
{
id: 'agent-3',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'gemini-2.5-pro' },
{ title: 'System Prompt', value: 'Draft follow-up email...' },
],
position: { x: 370, y: 150 },
},
{
id: 'gmail-1',
name: 'Gmail',
type: 'gmail',
bgColor: '#E0E0E0',
rows: [
{ title: 'Operation', value: 'Send Email' },
{ title: 'To', value: 'attendees' },
],
position: { x: 680, y: 60 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'gcal-1', target: 'agent-3' },
{ id: 'e-2', source: 'agent-3', target: 'gmail-1' },
],
}
/**
* CV/Resume Scanner — Start → Agent (Reducto) → Google Sheets
* Pattern: Convex (low → high → low)
*/
const CV_SCANNER_WORKFLOW: PreviewWorkflow = {
id: 'tpl-cv-scanner',
name: 'CV/Resume Scanner',
color: '#FA4EDF',
seedPath: '/landing-page-templates/resume-parser-d083c931-8788-4c6e-814c-0788830e164d.json',
blocks: [
{
id: 'starter-2',
name: 'Start',
type: 'starter',
bgColor: '#34B5FF',
rows: [{ title: 'File URL', value: 'resume.pdf' }],
position: { x: 60, y: 145 },
hideTargetHandle: true,
},
{
id: 'agent-4',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'claude-opus-4.6' },
{ title: 'System Prompt', value: 'Parse resume fields...' },
],
tools: [{ name: 'Reducto', type: 'reducto', bgColor: '#5c0c5c' }],
position: { x: 370, y: 55 },
},
{
id: 'gsheets-1',
name: 'Google Sheets',
type: 'google_sheets',
bgColor: '#E0E0E0',
rows: [
{ title: 'Spreadsheet', value: 'Candidates' },
{ title: 'Operation', value: 'Append Row' },
],
position: { x: 680, y: 145 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'starter-2', target: 'agent-4' },
{ id: 'e-2', source: 'agent-4', target: 'gsheets-1' },
],
}
/**
* Email Triage Agent — Gmail → Agent (KB) → fan-out to Slack + Linear
* Pattern: Fan-out (input low → agent mid → outputs spread vertically)
*/
const EMAIL_TRIAGE_WORKFLOW: PreviewWorkflow = {
id: 'tpl-email-triage',
name: 'Email Triage Agent',
color: '#FF6B2C',
seedPath: '/landing-page-templates/email-triage-57e84f83-c583-4e74-b73a-080d25f2074d.json',
blocks: [
{
id: 'gmail-2',
name: 'Gmail',
type: 'gmail',
bgColor: '#E0E0E0',
rows: [
{ title: 'Event', value: 'New Email' },
{ title: 'Label', value: 'Inbox' },
],
position: { x: 60, y: 130 },
hideTargetHandle: true,
},
{
id: 'agent-5',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'gpt-5.2-mini' },
{ title: 'System Prompt', value: 'Classify and route...' },
],
tools: [{ name: 'Knowledge Base', type: 'knowledge_base', bgColor: '#00B0B0' }],
position: { x: 370, y: 100 },
},
{
id: 'slack-2',
name: 'Slack',
type: 'slack',
bgColor: '#611f69',
rows: [
{ title: 'Channel', value: '#urgent' },
{ title: 'Operation', value: 'Send Message' },
],
position: { x: 680, y: 20 },
hideSourceHandle: true,
},
{
id: 'linear-1',
name: 'Linear',
type: 'linear',
bgColor: '#5E6AD2',
rows: [
{ title: 'Project', value: 'Support' },
{ title: 'Operation', value: 'Create Issue' },
],
position: { x: 680, y: 200 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'gmail-2', target: 'agent-5' },
{ id: 'e-2', source: 'agent-5', target: 'slack-2' },
{ id: 'e-3', source: 'agent-5', target: 'linear-1' },
],
}
/**
* Competitor Monitor — Schedule → Agent (Firecrawl) → Slack
* Pattern: Concave (high → low → high)
*/
const COMPETITOR_MONITOR_WORKFLOW: PreviewWorkflow = {
id: 'tpl-competitor-monitor',
name: 'Competitor Monitor',
color: '#6366F1',
seedPath: '/landing-page-templates/competitor-monitor-52454688-49ae-4279-894a-aa6494f10e3a.json',
blocks: [
{
id: 'schedule-1',
name: 'Schedule',
type: 'schedule',
bgColor: '#6366F1',
rows: [
{ title: 'Run Frequency', value: 'Daily' },
{ title: 'Time', value: '08:00 AM' },
],
position: { x: 60, y: 50 },
hideTargetHandle: true,
},
{
id: 'agent-6',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'grok-4' },
{ title: 'System Prompt', value: 'Monitor competitor...' },
],
tools: [{ name: 'Firecrawl', type: 'firecrawl', bgColor: '#181C1E' }],
position: { x: 370, y: 150 },
},
{
id: 'slack-3',
name: 'Slack',
type: 'slack',
bgColor: '#611f69',
rows: [
{ title: 'Channel', value: '#competitive-intel' },
{ title: 'Operation', value: 'Send Message' },
],
position: { x: 680, y: 50 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'schedule-1', target: 'agent-6' },
{ id: 'e-2', source: 'agent-6', target: 'slack-3' },
],
}
/**
* Social Listening Agent — Schedule → Agent (Reddit + X) → Notion
* Pattern: Convex (low → high → low)
*/
const SOCIAL_LISTENING_WORKFLOW: PreviewWorkflow = {
id: 'tpl-social-listening',
name: 'Social Listening Agent',
color: '#F43F5E',
seedPath: '/landing-page-templates/brand-mention-d2578496-d153-4db1-8ef1-c738cfa94a96.json',
blocks: [
{
id: 'schedule-2',
name: 'Schedule',
type: 'schedule',
bgColor: '#6366F1',
rows: [{ title: 'Run Frequency', value: 'Hourly' }],
position: { x: 60, y: 150 },
hideTargetHandle: true,
},
{
id: 'agent-7',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'gemini-2.5-flash' },
{ title: 'System Prompt', value: 'Track brand mentions...' },
],
tools: [
{ name: 'Reddit', type: 'reddit', bgColor: '#FF5700' },
{ name: 'X', type: 'x', bgColor: '#000000' },
],
position: { x: 370, y: 55 },
},
{
id: 'notion-1',
name: 'Notion',
type: 'notion',
bgColor: '#181C1E',
rows: [
{ title: 'Database', value: 'Brand Mentions' },
{ title: 'Operation', value: 'Create Page' },
],
position: { x: 680, y: 150 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'schedule-2', target: 'agent-7' },
{ id: 'e-2', source: 'agent-7', target: 'notion-1' },
],
}
/**
* Data Enrichment Pipeline — Start → Agent (LinkedIn) → Google Sheets
* Pattern: Concave (high → low → high)
*/
const DATA_ENRICHMENT_WORKFLOW: PreviewWorkflow = {
id: 'tpl-data-enrichment',
name: 'Data Enrichment Pipeline',
color: '#14B8A6',
seedPath: '/landing-page-templates/lead-enricher-6ed8dede-1df6-4962-95f4-887abf524b38.json',
blocks: [
{
id: 'starter-3',
name: 'Start',
type: 'starter',
bgColor: '#34B5FF',
rows: [{ title: 'Email', value: 'lead@company.com' }],
position: { x: 60, y: 55 },
hideTargetHandle: true,
},
{
id: 'agent-8',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'mistral-large' },
{ title: 'System Prompt', value: 'Enrich lead data...' },
],
tools: [{ name: 'LinkedIn', type: 'linkedin', bgColor: '#0072B1' }],
position: { x: 370, y: 145 },
},
{
id: 'gsheets-2',
name: 'Google Sheets',
type: 'google_sheets',
bgColor: '#E0E0E0',
rows: [
{ title: 'Spreadsheet', value: 'Lead Database' },
{ title: 'Operation', value: 'Update Row' },
],
position: { x: 680, y: 55 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'starter-3', target: 'agent-8' },
{ id: 'e-2', source: 'agent-8', target: 'gsheets-2' },
],
}
/**
* Customer Feedback Digest — Schedule → Agent → Slack
* Pattern: Convex (low → high → low)
*/
const FEEDBACK_DIGEST_WORKFLOW: PreviewWorkflow = {
id: 'tpl-feedback-digest',
name: 'Customer Feedback Digest',
color: '#F59E0B',
seedPath:
'/landing-page-templates/customer-feedback-digest-2a1c59de-fdcc-4ae0-b448-69a58b36c29a.json',
blocks: [
{
id: 'schedule-3',
name: 'Schedule',
type: 'schedule',
bgColor: '#6366F1',
rows: [
{ title: 'Run Frequency', value: 'Daily' },
{ title: 'Time', value: '09:00 AM' },
],
position: { x: 60, y: 145 },
hideTargetHandle: true,
},
{
id: 'agent-9',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'claude-sonnet-4.6' },
{ title: 'System Prompt', value: 'Analyze customer feedback...' },
],
tools: [{ name: 'Airtable', type: 'airtable', bgColor: '#18BFFF' }],
position: { x: 370, y: 50 },
},
{
id: 'slack-4',
name: 'Slack',
type: 'slack',
bgColor: '#611f69',
rows: [
{ title: 'Channel', value: '#product-feedback' },
{ title: 'Operation', value: 'Send Message' },
],
position: { x: 680, y: 145 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'schedule-3', target: 'agent-9' },
{ id: 'e-2', source: 'agent-9', target: 'slack-4' },
],
}
/**
* PR Review Agent — GitHub → Agent → Slack
* Pattern: Concave (high → low → high)
*/
const PR_REVIEW_WORKFLOW: PreviewWorkflow = {
id: 'tpl-pr-review',
name: 'PR Review Agent',
color: '#06B6D4',
seedPath: '/landing-page-templates/pr-review-cb5f2d92-a324-4958-8303-4710c572b71d.json',
blocks: [
{
id: 'github-2',
name: 'GitHub',
type: 'github',
bgColor: '#181C1E',
rows: [
{ title: 'Event', value: 'Pull Request Opened' },
{ title: 'Repository', value: 'org/repo' },
],
position: { x: 60, y: 60 },
hideTargetHandle: true,
},
{
id: 'agent-10',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'gpt-5.2' },
{ title: 'System Prompt', value: 'Review code changes...' },
],
position: { x: 370, y: 155 },
},
{
id: 'slack-5',
name: 'Slack',
type: 'slack',
bgColor: '#611f69',
rows: [
{ title: 'Channel', value: '#code-reviews' },
{ title: 'Operation', value: 'Send Message' },
],
position: { x: 680, y: 60 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'github-2', target: 'agent-10' },
{ id: 'e-2', source: 'agent-10', target: 'slack-5' },
],
}
/**
* Knowledge Base QA — Start → Agent (KB) → Response
* Pattern: Convex (low → high → low)
*/
const KNOWLEDGE_QA_WORKFLOW: PreviewWorkflow = {
id: 'tpl-knowledge-qa',
name: 'Knowledge Base QA',
color: '#84CC16',
seedPath: '/landing-page-templates/knowledge-base-qa-e9dcd9f1-18bd-4163-b5d8-3e239a297526.json',
blocks: [
{
id: 'starter-4',
name: 'Start',
type: 'starter',
bgColor: '#34B5FF',
rows: [{ title: 'Question', value: 'How do I...' }],
position: { x: 60, y: 140 },
hideTargetHandle: true,
},
{
id: 'agent-11',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'gemini-2.5-pro' },
{ title: 'System Prompt', value: 'Answer using knowledge...' },
],
tools: [{ name: 'Knowledge Base', type: 'knowledge_base', bgColor: '#00B0B0' }],
position: { x: 370, y: 50 },
},
{
id: 'starter-5',
name: 'Response',
type: 'starter',
bgColor: '#34B5FF',
rows: [{ title: 'Answer', value: 'Based on your docs...' }],
position: { x: 680, y: 140 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'starter-4', target: 'agent-11' },
{ id: 'e-2', source: 'agent-11', target: 'starter-5' },
],
}
export const TEMPLATE_WORKFLOWS: PreviewWorkflow[] = [
OCR_INVOICE_WORKFLOW,
GITHUB_RELEASE_WORKFLOW,
MEETING_FOLLOWUP_WORKFLOW,
CV_SCANNER_WORKFLOW,
EMAIL_TRIAGE_WORKFLOW,
COMPETITOR_MONITOR_WORKFLOW,
SOCIAL_LISTENING_WORKFLOW,
DATA_ENRICHMENT_WORKFLOW,
FEEDBACK_DIGEST_WORKFLOW,
PR_REVIEW_WORKFLOW,
KNOWLEDGE_QA_WORKFLOW,
]

View File

@@ -0,0 +1,596 @@
'use client'
import { useCallback, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { type MotionValue, motion, useScroll, useTransform } from 'framer-motion'
import dynamic from 'next/dynamic'
import { useRouter } from 'next/navigation'
import { Badge, ChevronDown } from '@/components/emcn'
import { LandingWorkflowSeedStorage } from '@/lib/core/utils/browser-storage'
import { cn } from '@/lib/core/utils/cn'
import { TEMPLATE_WORKFLOWS } from '@/app/(home)/components/templates/template-workflows'
const logger = createLogger('LandingTemplates')
const LandingPreviewWorkflow = dynamic(
() =>
import(
'@/app/(home)/components/landing-preview/components/landing-preview-workflow/landing-preview-workflow'
).then((mod) => mod.LandingPreviewWorkflow),
{
ssr: false,
loading: () => <div className='h-full w-full bg-[#1b1b1b]' />,
}
)
function hexToRgba(hex: string, alpha: number): string {
const r = Number.parseInt(hex.slice(1, 3), 16)
const g = Number.parseInt(hex.slice(3, 5), 16)
const b = Number.parseInt(hex.slice(5, 7), 16)
return `rgba(${r},${g},${b},${alpha})`
}
const LEFT_WALL_CLIP = 'polygon(0 8px, 100% 0, 100% 100%, 0 100%)'
const BOTTOM_WALL_CLIP = 'polygon(0 0, 100% 0, calc(100% - 8px) 100%, 0 100%)'
interface DepthConfig {
color: string
segments: readonly (readonly [opacity: number, width: number])[]
}
/** Depth color and gradient segment pattern per template. Segments are `[opacity, width%]` tuples. */
const DEPTH_CONFIGS: Record<string, DepthConfig> = {
'tpl-ocr-invoice': {
color: '#2ABBF8',
segments: [
[0.3, 10],
[0.5, 8],
[0.8, 6],
[1, 5],
[0.4, 12],
[0.7, 8],
[1, 6],
[0.5, 10],
[0.9, 7],
[0.6, 12],
[1, 8],
[0.35, 8],
],
},
'tpl-github-release': {
color: '#00F701',
segments: [
[0.4, 8],
[0.7, 6],
[1, 5],
[0.5, 14],
[0.85, 8],
[0.3, 12],
[1, 6],
[0.6, 10],
[0.9, 7],
[0.45, 8],
[1, 8],
[0.7, 8],
],
},
'tpl-meeting-followup': {
color: '#FFCC02',
segments: [
[0.5, 12],
[0.8, 6],
[0.35, 10],
[1, 5],
[0.6, 8],
[0.9, 7],
[0.4, 14],
[1, 6],
[0.7, 10],
[0.5, 8],
[1, 6],
[0.3, 8],
],
},
'tpl-cv-scanner': {
color: '#FA4EDF',
segments: [
[0.35, 6],
[0.6, 10],
[0.9, 5],
[1, 6],
[0.4, 8],
[0.75, 12],
[0.5, 7],
[1, 5],
[0.3, 10],
[0.8, 8],
[0.6, 9],
[1, 6],
[0.45, 8],
],
},
'tpl-email-triage': {
color: '#FF6B2C',
segments: [
[0.4, 10],
[0.7, 8],
[1, 5],
[0.5, 12],
[0.85, 6],
[0.3, 10],
[1, 6],
[0.6, 8],
[0.9, 7],
[0.4, 12],
[1, 8],
[0.65, 8],
],
},
'tpl-competitor-monitor': {
color: '#6366F1',
segments: [
[0.3, 8],
[0.55, 10],
[0.8, 6],
[1, 5],
[0.4, 12],
[0.7, 7],
[0.9, 8],
[0.5, 10],
[1, 6],
[0.35, 8],
[0.75, 6],
[1, 6],
[0.6, 8],
],
},
'tpl-social-listening': {
color: '#F43F5E',
segments: [
[0.5, 10],
[0.8, 6],
[0.4, 8],
[1, 5],
[0.6, 12],
[0.35, 8],
[0.9, 7],
[1, 6],
[0.5, 10],
[0.75, 8],
[0.4, 6],
[1, 6],
[0.65, 8],
],
},
'tpl-data-enrichment': {
color: '#14B8A6',
segments: [
[0.35, 8],
[0.6, 6],
[0.9, 5],
[0.4, 12],
[1, 6],
[0.7, 10],
[0.5, 7],
[0.85, 8],
[1, 5],
[0.3, 10],
[0.65, 8],
[1, 7],
[0.5, 8],
],
},
'tpl-feedback-digest': {
color: '#F59E0B',
segments: [
[0.4, 10],
[0.65, 6],
[0.9, 5],
[0.5, 12],
[1, 6],
[0.35, 8],
[0.75, 7],
[1, 5],
[0.6, 10],
[0.85, 8],
[0.45, 6],
[1, 8],
[0.55, 9],
],
},
'tpl-pr-review': {
color: '#06B6D4',
segments: [
[0.35, 8],
[0.7, 7],
[1, 5],
[0.45, 10],
[0.8, 6],
[0.3, 12],
[1, 6],
[0.55, 8],
[0.9, 7],
[0.4, 10],
[1, 6],
[0.65, 8],
[0.5, 7],
],
},
'tpl-knowledge-qa': {
color: '#84CC16',
segments: [
[0.5, 8],
[0.75, 6],
[0.4, 10],
[1, 5],
[0.6, 8],
[0.85, 7],
[0.35, 12],
[1, 6],
[0.7, 8],
[0.45, 10],
[0.9, 6],
[1, 6],
[0.55, 8],
],
},
}
const SCROLL_BLOCK_RX = '2.59574'
/**
* Two-row horizontal block strip for the scroll-driven reveal in the templates section.
* Same structural pattern as the hero's top-right blocks with matching colours:
* blue (left) → pink (middle) → green (right).
*/
const SCROLL_BLOCK_RECTS = [
{ opacity: 0.6, x: '-34.24', y: '0', width: '34.24', height: '16.86', fill: '#2ABBF8' },
{ opacity: 1, x: '-17.38', y: '0', width: '16.86', height: '16.86', fill: '#2ABBF8' },
{ opacity: 1, x: '0', y: '0', width: '16.86', height: '33.73', fill: '#2ABBF8' },
{ opacity: 0.6, x: '0', y: '0', width: '85.34', height: '16.86', fill: '#2ABBF8' },
{ opacity: 1, x: '0', y: '0', width: '16.86', height: '16.86', fill: '#2ABBF8' },
{ opacity: 0.6, x: '34.24', y: '0', width: '34.24', height: '33.73', fill: '#2ABBF8' },
{ opacity: 1, x: '34.24', y: '0', width: '16.86', height: '16.86', fill: '#2ABBF8' },
{ opacity: 1, x: '51.62', y: '16.86', width: '16.86', height: '16.86', fill: '#2ABBF8' },
{ opacity: 1, x: '68.48', y: '0', width: '54.65', height: '16.86', fill: '#FA4EDF' },
{ opacity: 0.6, x: '106.27', y: '0', width: '34.24', height: '33.73', fill: '#FA4EDF' },
{ opacity: 0.6, x: '106.27', y: '0', width: '51.10', height: '16.86', fill: '#FA4EDF' },
{ opacity: 1, x: '123.65', y: '16.86', width: '16.86', height: '16.86', fill: '#FA4EDF' },
{ opacity: 0.6, x: '157.37', y: '0', width: '34.24', height: '16.86', fill: '#FA4EDF' },
{ opacity: 1, x: '157.37', y: '0', width: '16.86', height: '16.86', fill: '#FA4EDF' },
{ opacity: 0.6, x: '209.0', y: '0', width: '68.48', height: '16.86', fill: '#00F701' },
{ opacity: 0.6, x: '209.14', y: '0', width: '16.86', height: '33.73', fill: '#00F701' },
{ opacity: 0.6, x: '243.23', y: '0', width: '34.24', height: '33.73', fill: '#00F701' },
{ opacity: 1, x: '243.23', y: '0', width: '16.86', height: '16.86', fill: '#00F701' },
{ opacity: 0.6, x: '260.10', y: '0', width: '34.04', height: '16.86', fill: '#00F701' },
{ opacity: 1, x: '260.61', y: '16.86', width: '16.86', height: '16.86', fill: '#00F701' },
] as const
const SCROLL_BLOCK_MAX_X = Math.max(...SCROLL_BLOCK_RECTS.map((r) => Number.parseFloat(r.x)))
const SCROLL_REVEAL_START = 0.05
const SCROLL_REVEAL_SPAN = 0.7
const SCROLL_FADE_IN = 0.03
function getScrollBlockThreshold(x: string): number {
const normalized = Number.parseFloat(x) / SCROLL_BLOCK_MAX_X
return SCROLL_REVEAL_START + (1 - normalized) * SCROLL_REVEAL_SPAN
}
interface ScrollBlockRectProps {
scrollYProgress: MotionValue<number>
rect: (typeof SCROLL_BLOCK_RECTS)[number]
}
/** Renders a single SVG rect whose opacity is driven by scroll progress. */
function ScrollBlockRect({ scrollYProgress, rect }: ScrollBlockRectProps) {
const threshold = getScrollBlockThreshold(rect.x)
const opacity = useTransform(
scrollYProgress,
[threshold, threshold + SCROLL_FADE_IN],
[0, rect.opacity]
)
return (
<motion.rect
x={rect.x}
y={rect.y}
width={rect.width}
height={rect.height}
rx={SCROLL_BLOCK_RX}
fill={rect.fill}
style={{ opacity }}
/>
)
}
function buildBottomWallStyle(config: DepthConfig) {
let pos = 0
const stops: string[] = []
for (const [opacity, width] of config.segments) {
const c = hexToRgba(config.color, opacity)
stops.push(`${c} ${pos}%`, `${c} ${pos + width}%`)
pos += width
}
return {
clipPath: BOTTOM_WALL_CLIP,
background: `linear-gradient(135deg, ${stops.join(', ')})`,
}
}
interface DotGridProps {
className?: string
cols: number
rows: number
gap?: number
}
function DotGrid({ className, cols, rows, gap = 0 }: DotGridProps) {
return (
<div
aria-hidden='true'
className={className}
style={{
display: 'grid',
gridTemplateColumns: `repeat(${cols}, 1fr)`,
gap,
placeItems: 'center',
}}
>
{Array.from({ length: cols * rows }, (_, i) => (
<div key={i} className='h-[2px] w-[2px] rounded-full bg-[#2A2A2A]' />
))}
</div>
)
}
const TEMPLATES_PANEL_ID = 'templates-panel'
export default function Templates() {
const sectionRef = useRef<HTMLDivElement>(null)
const [activeIndex, setActiveIndex] = useState(0)
const [isPreparingTemplate, setIsPreparingTemplate] = useState(false)
const router = useRouter()
const { scrollYProgress } = useScroll({
target: sectionRef,
offset: ['start 0.9', 'start 0.2'],
})
const activeWorkflow = TEMPLATE_WORKFLOWS[activeIndex]
const activeDepth = DEPTH_CONFIGS[activeWorkflow.id]
const handleUseTemplate = useCallback(async () => {
if (isPreparingTemplate) return
setIsPreparingTemplate(true)
try {
if (activeWorkflow.seedPath) {
const response = await fetch(activeWorkflow.seedPath)
if (!response.ok) {
throw new Error(`Failed to fetch template seed: ${response.status}`)
}
const workflowJson = await response.text()
LandingWorkflowSeedStorage.store({
templateId: activeWorkflow.id,
workflowName: activeWorkflow.name,
color: activeWorkflow.color,
workflowJson,
})
}
} catch (error) {
logger.error('Failed to prepare landing template workflow seed', {
templateId: activeWorkflow.id,
error,
})
} finally {
setIsPreparingTemplate(false)
router.push('/signup')
}
}, [
activeWorkflow.color,
activeWorkflow.id,
activeWorkflow.name,
activeWorkflow.seedPath,
isPreparingTemplate,
router,
])
return (
<section
ref={sectionRef}
id='templates'
aria-labelledby='templates-heading'
className='mt-[40px] mb-[80px]'
>
<p className='sr-only'>
Sim includes {TEMPLATE_WORKFLOWS.length} pre-built workflow templates covering OCR
processing, release management, meeting follow-ups, resume scanning, email triage,
competitor monitoring, social listening, data enrichment, feedback analysis, code review,
and knowledge base Q&amp;A. Each template connects real integrations and LLMs pick one,
customise it, and deploy in minutes.
</p>
<div className='bg-[#1C1C1C]'>
<DotGrid
className='border-[#2A2A2A] border-y bg-[#1C1C1C] p-[6px]'
cols={120}
rows={1}
gap={6}
/>
<div className='relative overflow-hidden'>
<div
aria-hidden='true'
className='pointer-events-none absolute top-0 right-0 z-20 hidden lg:block'
>
<svg
width={329}
height={34}
viewBox='-34 0 329 34'
fill='none'
xmlns='http://www.w3.org/2000/svg'
className='h-auto w-full'
>
{SCROLL_BLOCK_RECTS.map((r, i) => (
<ScrollBlockRect key={i} scrollYProgress={scrollYProgress} rect={r} />
))}
</svg>
</div>
<div className='px-[80px] pt-[100px]'>
<div className='flex flex-col items-start gap-[20px]'>
<Badge
variant='blue'
size='md'
dot
className='font-season uppercase tracking-[0.02em] transition-colors duration-200'
style={{
color: activeDepth.color,
backgroundColor: hexToRgba(activeDepth.color, 0.1),
}}
>
Templates
</Badge>
<h2
id='templates-heading'
className='font-[430] font-season text-[40px] text-white leading-[100%] tracking-[-0.02em]'
>
Ship your agent in minutes
</h2>
<p className='font-[430] font-season text-[#F6F6F0]/50 text-[16px] leading-[125%] tracking-[0.02em]'>
Pre-built templates for every use casepick one, swap <br />
models and tools to fit your stack, and deploy.
</p>
</div>
</div>
<div className='mt-[73px] flex border-[#2A2A2A] border-y'>
<DotGrid
className='w-[80px] shrink-0 overflow-hidden border-[#2A2A2A] border-r p-[6px]'
cols={6}
rows={55}
gap={6}
/>
<div className='flex min-w-0 flex-1'>
<div
role='tablist'
aria-label='Workflow templates'
className='flex w-[300px] shrink-0 flex-col border-[#2A2A2A] border-r'
>
{TEMPLATE_WORKFLOWS.map((workflow, index) => {
const isActive = index === activeIndex
return (
<button
key={workflow.id}
id={`template-tab-${index}`}
type='button'
role='tab'
aria-selected={isActive}
aria-controls={TEMPLATES_PANEL_ID}
onClick={() => setActiveIndex(index)}
className={cn(
'relative text-left',
isActive
? 'z-10'
: 'flex items-center px-[12px] py-[10px] shadow-[inset_0_-1px_0_0_#2A2A2A] last:shadow-none hover:bg-[#232323]/50'
)}
>
{isActive ? (
(() => {
const depth = DEPTH_CONFIGS[workflow.id]
return (
<>
<div
className='absolute top-[-8px] bottom-0 left-0 w-2'
style={{
clipPath: LEFT_WALL_CLIP,
backgroundColor: hexToRgba(depth.color, 0.63),
}}
/>
<div
className='absolute right-[-8px] bottom-0 left-2 h-2'
style={buildBottomWallStyle(depth)}
/>
<div className='-translate-y-2 relative flex translate-x-2 items-center bg-[#242424] px-[12px] py-[10px] shadow-[inset_0_0_0_1.5px_#3E3E3E]'>
<span className='flex-1 font-[430] font-season text-[16px] text-white'>
{workflow.name}
</span>
<ChevronDown
className='-rotate-90 h-[11px] w-[11px] shrink-0'
style={{ color: depth.color }}
/>
</div>
</>
)
})()
) : (
<span className='font-[430] font-season text-[#F6F6F0]/50 text-[16px]'>
{workflow.name}
</span>
)}
</button>
)
})}
</div>
<div
id={TEMPLATES_PANEL_ID}
role='tabpanel'
aria-labelledby={`template-tab-${activeIndex}`}
className='relative hidden flex-1 lg:block'
>
<div aria-hidden='true' className='h-full'>
<LandingPreviewWorkflow
key={activeIndex}
workflow={activeWorkflow}
animate
fitViewOptions={{ padding: 0.15, maxZoom: 1.3 }}
/>
</div>
<button
type='button'
onClick={handleUseTemplate}
disabled={isPreparingTemplate}
className='group/cta absolute top-[16px] right-[16px] z-10 inline-flex h-[32px] cursor-pointer items-center gap-[6px] rounded-[5px] border border-[#33C482] bg-[#33C482] px-[10px] font-[430] font-season text-[14px] text-black transition-[filter] hover:brightness-110'
>
{isPreparingTemplate ? 'Preparing...' : 'Use template'}
<span className='relative h-[10px] w-[10px] shrink-0'>
<ChevronDown className='-rotate-90 absolute inset-0 h-[10px] w-[10px] transition-opacity duration-150 group-hover/cta:opacity-0' />
<svg
className='absolute inset-0 h-[10px] w-[10px] opacity-0 transition-opacity duration-150 group-hover/cta:opacity-100'
viewBox='0 0 10 10'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M1 5H8M5.5 2L8.5 5L5.5 8'
stroke='currentColor'
strokeWidth='1.33'
strokeLinecap='square'
strokeLinejoin='miter'
fill='none'
/>
</svg>
</span>
</button>
</div>
</div>
<DotGrid
className='w-[80px] shrink-0 overflow-hidden border-[#2A2A2A] border-l p-[6px]'
cols={6}
rows={55}
gap={6}
/>
</div>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,18 @@
/**
* Testimonials section — social proof via user quotes.
*
* SEO:
* - `<section id="testimonials" aria-labelledby="testimonials-heading">`.
* - `<h2 id="testimonials-heading">` for the section title.
* - Each testimonial: `<blockquote cite="tweet-url">` with `<footer><cite>Author</cite></footer>`.
* - Profile images use `loading="lazy"` (below the fold).
*
* GEO:
* - Keep quote text as plain text in `<blockquote>` — not split across `<span>` elements.
* - Include full author name + handle (LLMs weigh attributed quotes higher).
* - Testimonials mentioning "Sim" by name carry more citation weight.
* - Review data here aligns with `review` entries in structured-data.tsx.
*/
export default function Testimonials() {
return null
}

View File

@@ -0,0 +1,53 @@
import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono'
import { season } from '@/app/_styles/fonts/season/season'
import {
Collaboration,
Enterprise,
Features,
Footer,
Hero,
Navbar,
Pricing,
StructuredData,
Templates,
Testimonials,
} from '@/app/(home)/components'
/**
* Landing page root component.
*
* ## SEO Architecture
* - Single `<h1>` inside Hero (only one per page).
* - Heading hierarchy: H1 (Hero) -> H2 (each section) -> H3 (sub-items).
* - Semantic landmarks: `<header>`, `<main>`, `<footer>`.
* - Every `<section>` has an `id` for anchor linking and `aria-labelledby` for accessibility.
* - `StructuredData` emits JSON-LD before any visible content.
*
* ## GEO Architecture
* - Above-fold content (Navbar, Hero) is statically rendered (Server Components where possible)
* for immediate availability to AI crawlers.
* - Section `id` attributes serve as fragment anchors for precise AI citations.
* - Content ordering prioritizes answer-first patterns: definition (Hero) ->
* examples (Templates) -> capabilities (Features) -> social proof (Collaboration, Testimonials) ->
* pricing (Pricing) -> enterprise (Enterprise).
*/
export default async function Landing() {
return (
<div className={`${season.variable} ${martianMono.variable} min-h-screen bg-[#1C1C1C]`}>
<StructuredData />
<header>
<Navbar />
</header>
<main>
<Hero />
<Templates />
<Features />
<Collaboration />
<Pricing />
<Enterprise />
<Testimonials />
</main>
<Footer />
</div>
)
}

View File

@@ -0,0 +1,18 @@
import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono'
import { season } from '@/app/_styles/fonts/season/season'
/**
* Landing page route-group layout.
*
* Applies landing-specific font CSS variables to the subtree:
* - `--font-season` (Season Sans): Headings and display text
* - `--font-martian-mono` (Martian Mono): Code snippets and technical accents
*
* Available to child components via Tailwind (`font-season`, `font-martian-mono`).
*
* SEO metadata for the `/` route is exported from `app/page.tsx` — not here.
* This layout only applies when a `page.tsx` exists inside the `(home)/` route group.
*/
export default function HomeLayout({ children }: { children: React.ReactNode }) {
return <div className={`${season.variable} ${martianMono.variable}`}>{children}</div>
}

View File

@@ -1,534 +0,0 @@
'use client'
import { useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { X } from 'lucide-react'
import { Textarea } from '@/components/emcn'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { isHosted } from '@/lib/core/config/feature-flags'
import { cn } from '@/lib/core/utils/cn'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
import Footer from '@/app/(landing)/components/footer/footer'
import Nav from '@/app/(landing)/components/nav/nav'
const logger = createLogger('CareersPage')
const validateName = (name: string): string[] => {
const errors: string[] = []
if (!name || name.trim().length < 2) {
errors.push('Name must be at least 2 characters')
}
return errors
}
const validateEmail = (email: string): string[] => {
const errors: string[] = []
if (!email || !email.trim()) {
errors.push('Email is required')
return errors
}
const validation = quickValidateEmail(email.trim().toLowerCase())
if (!validation.isValid) {
errors.push(validation.reason || 'Please enter a valid email address')
}
return errors
}
const validatePosition = (position: string): string[] => {
const errors: string[] = []
if (!position || position.trim().length < 2) {
errors.push('Please specify the position you are interested in')
}
return errors
}
const validateLinkedIn = (url: string): string[] => {
if (!url || url.trim() === '') return []
const errors: string[] = []
try {
new URL(url)
} catch {
errors.push('Please enter a valid LinkedIn URL')
}
return errors
}
const validatePortfolio = (url: string): string[] => {
if (!url || url.trim() === '') return []
const errors: string[] = []
try {
new URL(url)
} catch {
errors.push('Please enter a valid portfolio URL')
}
return errors
}
const validateLocation = (location: string): string[] => {
const errors: string[] = []
if (!location || location.trim().length < 2) {
errors.push('Please enter your location')
}
return errors
}
const validateMessage = (message: string): string[] => {
const errors: string[] = []
if (!message || message.trim().length < 50) {
errors.push('Please tell us more about yourself (at least 50 characters)')
}
return errors
}
export default function CareersPage() {
const [isSubmitting, setIsSubmitting] = useState(false)
const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle')
const [showErrors, setShowErrors] = useState(false)
// Form fields
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [phone, setPhone] = useState('')
const [position, setPosition] = useState('')
const [linkedin, setLinkedin] = useState('')
const [portfolio, setPortfolio] = useState('')
const [experience, setExperience] = useState('')
const [location, setLocation] = useState('')
const [message, setMessage] = useState('')
const [resume, setResume] = useState<File | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
// Field errors
const [nameErrors, setNameErrors] = useState<string[]>([])
const [emailErrors, setEmailErrors] = useState<string[]>([])
const [positionErrors, setPositionErrors] = useState<string[]>([])
const [linkedinErrors, setLinkedinErrors] = useState<string[]>([])
const [portfolioErrors, setPortfolioErrors] = useState<string[]>([])
const [experienceErrors, setExperienceErrors] = useState<string[]>([])
const [locationErrors, setLocationErrors] = useState<string[]>([])
const [messageErrors, setMessageErrors] = useState<string[]>([])
const [resumeErrors, setResumeErrors] = useState<string[]>([])
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] || null
setResume(file)
if (file) {
setResumeErrors([])
}
}
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setShowErrors(true)
// Validate all fields
const nameErrs = validateName(name)
const emailErrs = validateEmail(email)
const positionErrs = validatePosition(position)
const linkedinErrs = validateLinkedIn(linkedin)
const portfolioErrs = validatePortfolio(portfolio)
const experienceErrs = experience ? [] : ['Please select your years of experience']
const locationErrs = validateLocation(location)
const messageErrs = validateMessage(message)
const resumeErrs = resume ? [] : ['Resume is required']
setNameErrors(nameErrs)
setEmailErrors(emailErrs)
setPositionErrors(positionErrs)
setLinkedinErrors(linkedinErrs)
setPortfolioErrors(portfolioErrs)
setExperienceErrors(experienceErrs)
setLocationErrors(locationErrs)
setMessageErrors(messageErrs)
setResumeErrors(resumeErrs)
if (
nameErrs.length > 0 ||
emailErrs.length > 0 ||
positionErrs.length > 0 ||
linkedinErrs.length > 0 ||
portfolioErrs.length > 0 ||
experienceErrs.length > 0 ||
locationErrs.length > 0 ||
messageErrs.length > 0 ||
resumeErrs.length > 0
) {
return
}
setIsSubmitting(true)
setSubmitStatus('idle')
try {
const formData = new FormData()
formData.append('name', name)
formData.append('email', email)
formData.append('phone', phone || '')
formData.append('position', position)
formData.append('linkedin', linkedin || '')
formData.append('portfolio', portfolio || '')
formData.append('experience', experience)
formData.append('location', location)
formData.append('message', message)
if (resume) formData.append('resume', resume)
const response = await fetch('/api/careers/submit', {
method: 'POST',
body: formData,
})
if (!response.ok) {
throw new Error('Failed to submit application')
}
setSubmitStatus('success')
} catch (error) {
logger.error('Error submitting application:', error)
setSubmitStatus('error')
} finally {
setIsSubmitting(false)
}
}
return (
<main className={`${soehne.className} min-h-screen bg-white text-gray-900`}>
<Nav variant='landing' />
{/* Content */}
<div className='px-4 pt-[60px] pb-[80px] sm:px-8 md:px-[44px]'>
<h1 className='mb-10 text-center font-bold text-4xl text-gray-900 md:text-5xl'>
Join Our Team
</h1>
<div className='mx-auto max-w-4xl'>
{/* Form Section */}
<section className='rounded-2xl border border-gray-200 bg-white p-6 shadow-sm sm:p-10'>
<form onSubmit={onSubmit} className='space-y-5'>
{/* Name and Email */}
<div className='grid grid-cols-1 gap-4 sm:gap-6 md:grid-cols-2'>
<div className='space-y-2'>
<Label htmlFor='name' className='font-medium text-sm'>
Full Name *
</Label>
<Input
id='name'
placeholder='John Doe'
value={name}
onChange={(e) => setName(e.target.value)}
className={cn(
showErrors &&
nameErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && nameErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{nameErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
<div className='space-y-2'>
<Label htmlFor='email' className='font-medium text-sm'>
Email *
</Label>
<Input
id='email'
type='email'
placeholder='john@example.com'
value={email}
onChange={(e) => setEmail(e.target.value)}
className={cn(
showErrors &&
emailErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && emailErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{emailErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
</div>
{/* Phone and Position */}
<div className='grid grid-cols-1 gap-4 sm:gap-6 md:grid-cols-2'>
<div className='space-y-2'>
<Label htmlFor='phone' className='font-medium text-sm'>
Phone Number
</Label>
<Input
id='phone'
type='tel'
placeholder='+1 (555) 123-4567'
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
</div>
<div className='space-y-2'>
<Label htmlFor='position' className='font-medium text-sm'>
Position of Interest *
</Label>
<Input
id='position'
placeholder='e.g. Full Stack Engineer, Product Designer'
value={position}
onChange={(e) => setPosition(e.target.value)}
className={cn(
showErrors &&
positionErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && positionErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{positionErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
</div>
{/* LinkedIn and Portfolio */}
<div className='grid grid-cols-1 gap-4 sm:gap-6 md:grid-cols-2'>
<div className='space-y-2'>
<Label htmlFor='linkedin' className='font-medium text-sm'>
LinkedIn Profile
</Label>
<Input
id='linkedin'
placeholder='https://linkedin.com/in/yourprofile'
value={linkedin}
onChange={(e) => setLinkedin(e.target.value)}
className={cn(
showErrors &&
linkedinErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && linkedinErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{linkedinErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
<div className='space-y-2'>
<Label htmlFor='portfolio' className='font-medium text-sm'>
Portfolio / Website
</Label>
<Input
id='portfolio'
placeholder='https://yourportfolio.com'
value={portfolio}
onChange={(e) => setPortfolio(e.target.value)}
className={cn(
showErrors &&
portfolioErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && portfolioErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{portfolioErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
</div>
{/* Experience and Location */}
<div className='grid grid-cols-1 gap-4 sm:gap-6 md:grid-cols-2'>
<div className='space-y-2'>
<Label htmlFor='experience' className='font-medium text-sm'>
Years of Experience *
</Label>
<Select value={experience} onValueChange={setExperience}>
<SelectTrigger
className={cn(
showErrors &&
experienceErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
>
<SelectValue placeholder='Select experience level' />
</SelectTrigger>
<SelectContent>
<SelectItem value='0-1'>0-1 years</SelectItem>
<SelectItem value='1-3'>1-3 years</SelectItem>
<SelectItem value='3-5'>3-5 years</SelectItem>
<SelectItem value='5-10'>5-10 years</SelectItem>
<SelectItem value='10+'>10+ years</SelectItem>
</SelectContent>
</Select>
{showErrors && experienceErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{experienceErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
<div className='space-y-2'>
<Label htmlFor='location' className='font-medium text-sm'>
Location *
</Label>
<Input
id='location'
placeholder='e.g. San Francisco, CA'
value={location}
onChange={(e) => setLocation(e.target.value)}
className={cn(
showErrors &&
locationErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && locationErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{locationErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
</div>
{/* Message */}
<div className='space-y-2'>
<Label htmlFor='message' className='font-medium text-sm'>
Tell us about yourself *
</Label>
<Textarea
id='message'
placeholder='Tell us about your experience, what excites you about Sim, and why you would be a great fit for this role...'
className={cn(
'min-h-[140px]',
showErrors &&
messageErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<p className='mt-1.5 text-gray-500 text-xs'>Minimum 50 characters</p>
{showErrors && messageErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{messageErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
{/* Resume Upload */}
<div className='space-y-2'>
<Label htmlFor='resume' className='font-medium text-sm'>
Resume *
</Label>
<div className='relative'>
{resume ? (
<div className='flex items-center gap-2 rounded-md border border-input bg-background px-3 py-2'>
<span className='flex-1 truncate text-sm'>{resume.name}</span>
<button
type='button'
onClick={(e) => {
e.preventDefault()
setResume(null)
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
}}
className='flex-shrink-0 text-muted-foreground transition-colors hover:text-foreground'
aria-label='Remove file'
>
<X className='h-4 w-4' />
</button>
</div>
) : (
<Input
id='resume'
type='file'
accept='.pdf,.doc,.docx'
onChange={handleFileChange}
ref={fileInputRef}
className={cn(
showErrors &&
resumeErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
)}
</div>
<p className='mt-1.5 text-gray-500 text-xs'>PDF or Word document, max 10MB</p>
{showErrors && resumeErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{resumeErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
{/* Submit Button */}
<div className='flex justify-end pt-2'>
<BrandedButton
type='submit'
disabled={isSubmitting || submitStatus === 'success'}
loading={isSubmitting}
loadingText='Submitting'
showArrow={false}
fullWidth={false}
className='min-w-[200px]'
>
{submitStatus === 'success' ? 'Submitted' : 'Submit Application'}
</BrandedButton>
</div>
</form>
</section>
{/* Additional Info */}
<section className='mt-6 text-center text-gray-600 text-sm'>
<p>
Questions? Email us at{' '}
<a
href='mailto:careers@sim.ai'
className='font-medium text-gray-900 underline transition-colors hover:text-gray-700'
>
careers@sim.ai
</a>
</p>
</section>
</div>
</div>
{/* Footer - Only for hosted instances */}
{isHosted && (
<div className='relative z-20'>
<Footer fullWidth={true} />
</div>
)}
</main>
)
}

View File

@@ -77,12 +77,14 @@ export default function Footer({ fullWidth = false }: FooterProps) {
>
Status
</Link>
<Link
href='/careers'
<a
href='https://jobs.ashbyhq.com/sim'
target='_blank'
rel='noopener noreferrer'
className='text-[14px] text-muted-foreground transition-colors hover:text-foreground'
>
Careers
</Link>
</a>
<Link
href='/privacy'
target='_blank'

View File

@@ -11,16 +11,14 @@ import {
Database,
DollarSign,
HardDrive,
RefreshCw,
Timer,
Zap,
} from 'lucide-react'
import { useRouter } from 'next/navigation'
import { cn } from '@/lib/core/utils/cn'
import { inter } from '@/app/_styles/fonts/inter/inter'
import {
ENTERPRISE_PLAN_FEATURES,
PRO_PLAN_FEATURES,
TEAM_PLAN_FEATURES,
} from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/plan-configs'
import { ENTERPRISE_PLAN_FEATURES } from '@/app/workspace/[workspaceId]/settings/components/subscription/plan-configs'
const logger = createLogger('LandingPricing')
@@ -38,20 +36,30 @@ interface PricingTier {
featured?: boolean
}
/**
* Free plan features with consistent icons
*/
const FREE_PLAN_FEATURES: PricingFeature[] = [
{ icon: DollarSign, text: '$20 usage limit' },
{ icon: DollarSign, text: '3,000 credits/mo' },
{ icon: HardDrive, text: '5GB file storage' },
{ icon: Timer, text: '5 min execution limit' },
{ icon: Database, text: 'Limited log retention' },
{ icon: Code2, text: 'CLI/SDK Access' },
]
/**
* Available pricing tiers with their features and pricing
*/
const PRO_LANDING_FEATURES: PricingFeature[] = [
{ icon: DollarSign, text: '6,000 credits/mo' },
{ icon: RefreshCw, text: '+50 daily refresh credits' },
{ icon: Zap, text: '150 runs/min (sync)' },
{ icon: Timer, text: '50 min sync execution limit' },
{ icon: HardDrive, text: '50GB file storage' },
]
const MAX_LANDING_FEATURES: PricingFeature[] = [
{ icon: DollarSign, text: '25,000 credits/mo' },
{ icon: RefreshCw, text: '+200 daily refresh credits' },
{ icon: Zap, text: '300 runs/min (sync)' },
{ icon: Timer, text: '50 min sync execution limit' },
{ icon: HardDrive, text: '500GB file storage' },
]
const pricingTiers: PricingTier[] = [
{
name: 'COMMUNITY',
@@ -63,16 +71,16 @@ const pricingTiers: PricingTier[] = [
{
name: 'PRO',
tier: 'Pro',
price: '$20/mo',
features: PRO_PLAN_FEATURES,
price: '$25/mo',
features: PRO_LANDING_FEATURES,
ctaText: 'Get Started',
featured: true,
},
{
name: 'TEAM',
tier: 'Team',
price: '$40/mo',
features: TEAM_PLAN_FEATURES,
name: 'MAX',
tier: 'Max',
price: '$100/mo',
features: MAX_LANDING_FEATURES,
ctaText: 'Get Started',
},
{
@@ -84,12 +92,6 @@ const pricingTiers: PricingTier[] = [
},
]
/**
* Individual pricing card component
* @param tier - The pricing tier data
* @param index - The index of the card in the grid
* @param isBeforeFeatured - Whether this card is immediately before a featured card
*/
function PricingCard({
tier,
index,
@@ -106,10 +108,8 @@ function PricingCard({
logger.info(`Pricing CTA clicked: ${tier.name}`)
if (tier.ctaText === 'Contact Sales') {
// Open enterprise form in new tab
window.open('https://form.typeform.com/to/jqCO12pF', '_blank')
} else {
// Navigate to signup page for all "Get Started" buttons
router.push('/signup')
}
}

View File

@@ -91,12 +91,14 @@ export default function Nav({ hideAuthButtons = false, variant = 'landing' }: Na
</button>
</li>
<li>
<Link
href='/careers'
<a
href='https://jobs.ashbyhq.com/sim'
target='_blank'
rel='noopener noreferrer'
className='text-[16px] text-muted-foreground transition-colors hover:text-foreground'
>
Careers
</Link>
</a>
</li>
<li>
<a

View File

@@ -8,7 +8,7 @@ export default function StructuredData() {
name: 'Sim',
alternateName: 'Sim',
description:
'Open-source AI agent workflow builder used by developers at trail-blazing startups to Fortune 500 companies',
'Sim is 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.',
url: 'https://sim.ai',
logo: {
'@type': 'ImageObject',
@@ -36,9 +36,9 @@ export default function StructuredData() {
'@type': 'WebSite',
'@id': 'https://sim.ai/#website',
url: 'https://sim.ai',
name: 'Sim - AI Agent Workflow Builder',
name: 'Sim — Build AI Agents & Run Your Agentic Workforce',
description:
'Open-source AI agent workflow builder. 70,000+ developers build and deploy agentic workflows. SOC2 and HIPAA compliant.',
'Sim is 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. Join 100,000+ builders.',
publisher: {
'@id': 'https://sim.ai/#organization',
},
@@ -48,7 +48,7 @@ export default function StructuredData() {
'@type': 'WebPage',
'@id': 'https://sim.ai/#webpage',
url: 'https://sim.ai',
name: 'Sim - Workflows for LLMs | Build AI Agent Workflows',
name: 'Sim — Build AI Agents & Run Your Agentic Workforce',
isPartOf: {
'@id': 'https://sim.ai/#website',
},
@@ -58,7 +58,7 @@ export default function StructuredData() {
datePublished: '2024-01-01T00:00:00+00:00',
dateModified: new Date().toISOString(),
description:
'Build and deploy AI agent workflows with Sim. Visual drag-and-drop interface for creating powerful LLM-powered automations.',
'Sim is 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. Create agents, workflows, knowledge bases, tables, and docs.',
breadcrumb: {
'@id': 'https://sim.ai/#breadcrumb',
},
@@ -85,9 +85,9 @@ export default function StructuredData() {
{
'@type': 'SoftwareApplication',
'@id': 'https://sim.ai/#software',
name: 'Sim - AI Agent Workflow Builder',
name: 'Sim — Build AI Agents & Run Your Agentic Workforce',
description:
'Open-source AI agent workflow builder used by 70,000+ developers. Build agentic workflows with visual drag-and-drop interface. SOC2 and HIPAA compliant. Integrate with 100+ apps.',
'Sim is 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. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders. SOC2 and HIPAA compliant.',
applicationCategory: 'DeveloperApplication',
applicationSubCategory: 'AI Development Tools',
operatingSystem: 'Web, Windows, macOS, Linux',
@@ -159,12 +159,13 @@ export default function StructuredData() {
worstRating: '1',
},
featureList: [
'Visual workflow builder',
'Drag-and-drop interface',
'100+ integrations',
'AI model support (OpenAI, Anthropic, Google, xAI, Mistral, Perplexity)',
'Real-time collaboration',
'Version control',
'AI agent creation',
'Agentic workflow orchestration',
'1,000+ integrations',
'LLM orchestration (OpenAI, Anthropic, Google, xAI, Mistral, Perplexity)',
'Knowledge base creation',
'Table creation',
'Document creation',
'API access',
'Custom functions',
'Scheduled workflows',
@@ -174,7 +175,7 @@ export default function StructuredData() {
{
'@type': 'ImageObject',
url: 'https://sim.ai/logo/426-240/primary/small.png',
caption: 'Sim AI agent workflow builder interface',
caption: 'Sim — build AI agents and run your agentic workforce',
},
],
},
@@ -187,7 +188,7 @@ export default function StructuredData() {
name: 'What is Sim?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Sim is an open-source AI agent workflow builder used by 70,000+ developers at trail-blazing startups to Fortune 500 companies. It provides a visual drag-and-drop interface for building and deploying agentic workflows. Sim is SOC2 and HIPAA compliant.',
text: 'Sim is the open-source platform to build AI agents and run your agentic workforce. Teams connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders. SOC2 and HIPAA compliant.',
},
},
{
@@ -203,7 +204,7 @@ export default function StructuredData() {
name: 'Do I need coding skills to use Sim?',
acceptedAnswer: {
'@type': 'Answer',
text: 'No coding skills are required! Sim features a visual drag-and-drop interface that makes it easy to build AI workflows. However, developers can also use custom functions and our API for advanced use cases.',
text: 'No coding skills are required. Sim provides a visual interface for building AI agents and agentic workflows. Developers can also use custom functions, the API, and the CLI/SDK for advanced use cases.',
},
},
],

View File

@@ -4,8 +4,8 @@ export const metadata: Metadata = {
metadataBase: new URL('https://sim.ai'),
manifest: '/manifest.json',
icons: {
icon: '/favicon.ico',
apple: '/apple-icon.png',
icon: [{ url: '/icon.svg', type: 'image/svg+xml', sizes: 'any' }],
apple: '/favicon/apple-touch-icon.png',
},
other: {
'msapplication-TileColor': '#000000',

View File

@@ -1,3 +1,4 @@
import type { Metadata } from 'next'
import Image from 'next/image'
import Link from 'next/link'
import { getAllPostMeta } from '@/lib/blog/registry'
@@ -5,6 +6,17 @@ import { soehne } from '@/app/_styles/fonts/soehne/soehne'
export const revalidate = 3600
export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>
}): Promise<Metadata> {
const { id } = await params
const posts = (await getAllPostMeta()).filter((p) => p.author.id === id)
const author = posts[0]?.author
return { title: author?.name ?? 'Author' }
}
export default async function AuthorPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const posts = (await getAllPostMeta()).filter((p) => p.author.id === id)

View File

@@ -1,8 +1,14 @@
import type { Metadata } from 'next'
import Link from 'next/link'
import { getAllPostMeta } from '@/lib/blog/registry'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { PostGrid } from '@/app/(landing)/studio/post-grid'
export const metadata: Metadata = {
title: 'Studio',
description: 'Announcements, insights, and guides from the Sim team.',
}
export const revalidate = 3600
export default async function StudioIndex({

View File

@@ -1,6 +1,11 @@
import type { Metadata } from 'next'
import Link from 'next/link'
import { getAllTags } from '@/lib/blog/registry'
export const metadata: Metadata = {
title: 'Tags',
}
export default async function TagsIndex() {
const tags = await getAllTags()
return (

View File

@@ -15,26 +15,41 @@ const BROWSER_EXTENSION_ATTRIBUTES = [
]
/**
* Client component that intercepts console.error to filter and log hydration errors
* while ignoring errors caused by browser extensions.
* Checks whether a hydration error is caused by Radix UI's auto-generated IDs
* (`aria-controls`, `id`) differing between server and client. This is a known
* harmless artifact of React 19's streaming SSR producing different `useId()`
* tree paths. The IDs self-correct on first interaction and have no visual or
* functional impact since they only link closed (invisible) popover/menu content.
*/
function isRadixIdMismatch(args: unknown[]): boolean {
return args.some(
(arg) => typeof arg === 'string' && arg.includes('radix-') && /aria-controls|"\bid\b"/.test(arg)
)
}
/**
* Client component that intercepts console.error to filter hydration errors
* caused by browser extensions or Radix UI's `useId()` mismatch.
*/
export function HydrationErrorHandler() {
useEffect(() => {
const originalError = console.error
console.error = (...args) => {
if (args[0].includes('Hydration')) {
if (typeof args[0] === 'string' && args[0].includes('Hydration')) {
const isExtensionError = BROWSER_EXTENSION_ATTRIBUTES.some((attr) =>
args.some((arg) => typeof arg === 'string' && arg.includes(attr))
)
if (!isExtensionError) {
logger.error('Hydration Error', {
details: args,
componentStack: args.find(
(arg) => typeof arg === 'string' && arg.includes('component stack')
),
})
if (isExtensionError || isRadixIdMismatch(args)) {
return
}
logger.error('Hydration Error', {
details: args,
componentStack: args.find(
(arg) => typeof arg === 'string' && arg.includes('component stack')
),
})
}
originalError.apply(console, args)
}

View File

@@ -0,0 +1,38 @@
import { defaultShouldDehydrateQuery, isServer, QueryClient } from '@tanstack/react-query'
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 30 * 1000,
gcTime: 5 * 60 * 1000,
refetchOnWindowFocus: false,
retry: 1,
retryOnMount: false,
},
mutations: {
retry: 1,
},
dehydrate: {
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
},
},
})
}
let browserQueryClient: QueryClient | undefined
/**
* Returns a QueryClient instance. On the server, creates a new instance per request.
* On the client, reuses a singleton instance.
*/
export function getQueryClient() {
if (isServer) {
return makeQueryClient()
}
if (!browserQueryClient) {
browserQueryClient = makeQueryClient()
}
return browserQueryClient
}

Some files were not shown because too many files have changed in this diff Show More