Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .agents/skills/add-integration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -872,7 +872,12 @@ export const {service}UploadTool: ToolConfig<Params, Response> = {
fileContent: { type: 'string', required: false, visibility: 'hidden' }, // Legacy
},
request: {
url: '/api/tools/{service}/upload', // Internal route
// Internal route. A static string is a source literal, so the transport trusts it. When the
// path is dynamic, use `internalRoute` from '@/lib/core/utils/internal-route' instead of a template
// string — a builder's plain `/api/...` string is treated as external, because a caller- or
// model-supplied param can produce one:
// url: (params) => internalRoute`/api/tools/{service}/upload/${params.folderId}`
url: '/api/tools/{service}/upload',
method: 'POST',
body: (params) => ({
accessToken: params.accessToken,
Expand Down
31 changes: 31 additions & 0 deletions .agents/skills/add-tools/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,37 @@ export const {serviceName}{Action}Tool: ToolConfig<
- Always explicitly set `required: true` or `required: false`
- Optional params should have `required: false`

## Internal Routes (calling Sim's own API)

Most tools call a third-party service and `request.url` returns an absolute `https://...` URL. A
tool that instead calls Sim's own API — a `/api/tools/{service}/{action}` proxy route, or a platform
route like `/api/table/...` — must SAY SO, because the transport resolves those against the internal
base URL and signs them with an internal token for the executing user.

That declaration comes from the tool's source, never from the resolved string: a `user-or-llm` param
can make any tool emit `/api/...` (the HTTP Request tool passes its `url` through verbatim, and a
self-hosted integration with a blank host param collapses `${host}/api/v2/x` to `/api/v2/x`).

```typescript
import { internalRoute } from '@/lib/core/utils/internal-route'

// ✓ Static route — a source literal no param can influence
url: '/api/tools/{service}/{action}',

// ✓ Dynamic route — branded, and every interpolated id is percent-encoded for you
url: (params) => internalRoute`/api/table/${params.tableId}/rows`,

// ✓ Query params via withQuery (accepts an object or URLSearchParams; skips undefined/null)
url: (params) => internalRoute`/api/logs`.withQuery({ workspaceId, limit: params.limit }),

// ✗ Treated as EXTERNAL and will fail — a builder's plain string carries no provenance
url: (params) => `/api/table/${params.tableId}/rows`,
```

`internalRoute` throws on a path outside `/api/` and on a query string inside the template. Never
write `encodeURIComponent` inside the template — the tag already encodes each `${...}`, so doing it
yourself double-encodes the value.

## Resolved Secrets and Provenance Boundaries

- Leave ordinary external API inputs and third-party results unchanged. Add provenance handling only
Expand Down
9 changes: 9 additions & 0 deletions .agents/skills/validate-integration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ For **every** tool file, check:
- [ ] Tool `description` is a concise one-liner describing what it does
- [ ] Tool `version` is set (`'1.0.0'` or `'2.0.0'` for V2)

### Request URL
- [ ] A tool calling a third-party service returns an ABSOLUTE `https://...` URL
- [ ] A tool calling Sim's own API declares it: a static `/api/...` string, or `internalRoute` from
`@/lib/core/utils/internal-route` when the path is dynamic (query params via `.withQuery({...})`)
- [ ] No builder returns a bare `` `/api/...` `` template string — that is treated as external and
will fail, because a `user-or-llm` param can produce the same string
- [ ] No `encodeURIComponent` inside an `internalRoute` template (the tag already encodes, so this
double-encodes the value)

### Params
- [ ] All required API params are marked `required: true`
- [ ] All optional API params are marked `required: false`
Expand Down
7 changes: 6 additions & 1 deletion .claude/commands/add-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -871,7 +871,12 @@ export const {service}UploadTool: ToolConfig<Params, Response> = {
fileContent: { type: 'string', required: false, visibility: 'hidden' }, // Legacy
},
request: {
url: '/api/tools/{service}/upload', // Internal route
// Internal route. A static string is a source literal, so the transport trusts it. When the
// path is dynamic, use `internalRoute` from '@/lib/core/utils/internal-route' instead of a template
// string — a builder's plain `/api/...` string is treated as external, because a caller- or
// model-supplied param can produce one:
// url: (params) => internalRoute`/api/tools/{service}/upload/${params.folderId}`
url: '/api/tools/{service}/upload',
method: 'POST',
body: (params) => ({
accessToken: params.accessToken,
Expand Down
31 changes: 31 additions & 0 deletions .claude/commands/add-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,37 @@ export const {serviceName}{Action}Tool: ToolConfig<
- Always explicitly set `required: true` or `required: false`
- Optional params should have `required: false`

## Internal Routes (calling Sim's own API)

Most tools call a third-party service and `request.url` returns an absolute `https://...` URL. A
tool that instead calls Sim's own API — a `/api/tools/{service}/{action}` proxy route, or a platform
route like `/api/table/...` — must SAY SO, because the transport resolves those against the internal
base URL and signs them with an internal token for the executing user.

That declaration comes from the tool's source, never from the resolved string: a `user-or-llm` param
can make any tool emit `/api/...` (the HTTP Request tool passes its `url` through verbatim, and a
self-hosted integration with a blank host param collapses `${host}/api/v2/x` to `/api/v2/x`).

```typescript
import { internalRoute } from '@/lib/core/utils/internal-route'

// ✓ Static route — a source literal no param can influence
url: '/api/tools/{service}/{action}',

// ✓ Dynamic route — branded, and every interpolated id is percent-encoded for you
url: (params) => internalRoute`/api/table/${params.tableId}/rows`,

// ✓ Query params via withQuery (accepts an object or URLSearchParams; skips undefined/null)
url: (params) => internalRoute`/api/logs`.withQuery({ workspaceId, limit: params.limit }),

// ✗ Treated as EXTERNAL and will fail — a builder's plain string carries no provenance
url: (params) => `/api/table/${params.tableId}/rows`,
```

`internalRoute` throws on a path outside `/api/` and on a query string inside the template. Never
write `encodeURIComponent` inside the template — the tag already encodes each `${...}`, so doing it
yourself double-encodes the value.

## Resolved Secrets and Provenance Boundaries

- Leave ordinary external API inputs and third-party results unchanged. Add provenance handling only
Expand Down
9 changes: 9 additions & 0 deletions .claude/commands/validate-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ For **every** tool file, check:
- [ ] Tool `description` is a concise one-liner describing what it does
- [ ] Tool `version` is set (`'1.0.0'` or `'2.0.0'` for V2)

### Request URL
- [ ] A tool calling a third-party service returns an ABSOLUTE `https://...` URL
- [ ] A tool calling Sim's own API declares it: a static `/api/...` string, or `internalRoute` from
`@/lib/core/utils/internal-route` when the path is dynamic (query params via `.withQuery({...})`)
- [ ] No builder returns a bare `` `/api/...` `` template string — that is treated as external and
will fail, because a `user-or-llm` param can produce the same string
- [ ] No `encodeURIComponent` inside an `internalRoute` template (the tag already encodes, so this
double-encodes the value)

### Params
- [ ] All required API params are marked `required: true`
- [ ] All optional API params are marked `required: false`
Expand Down
1 change: 1 addition & 0 deletions .claude/rules/sim-integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The full authoring instructions — tool/block/icon/trigger scaffolding, SubBloc
## Hard rules (don't get these wrong)

- Tool IDs are `snake_case` (`service_action`). Register tools in `tools/registry.ts`, blocks in `blocks/registry-maps.ts` (the `BLOCK_REGISTRY` config map + `BLOCK_META_REGISTRY` catalog-meta map, alphabetically — `blocks/registry.ts` holds only the accessor functions), triggers in `triggers/registry.ts`.
- A tool that calls Sim's own API declares it: a static `request.url` string (`'/api/tools/{service}/{action}'`), or `` internalRoute`/api/table/${params.tableId}/rows` `` from `@/lib/core/utils/internal-route` when the path is dynamic (query params via `.withQuery({...})`). The transport signs internal requests with the executing user's token, so that decision follows the tool's source, never the resolved string — a builder returning a bare `/api/...` string is treated as EXTERNAL, because a `user-or-llm` param can produce one. `internalRoute` encodes every `${...}`; never add `encodeURIComponent` inside the template.
- Type coercions (`Number()`, etc.) belong in `tools.config.params` (runs at execution, after variable resolution) — never in `tools.config.tool` (runs at serialization; coercing there destroys dynamic `<Block.output>` references).
- `canonicalParamId` must NOT match any subblock's `id`, must be unique **block-wide** (groups are keyed by canonical id across every subblock and hold exactly one `basicId`, so two operations that each need a pair need two different canonical ids), and all subblocks in a canonical group must share the same `required` status. The `inputs` section and the params function reference canonical IDs, not raw subblock IDs — the serializer deletes the subblock IDs and republishes the active member's value under the canonical ID.
- A canonical pair carries ONE concept. For files that is upload (basic) + file reference (advanced), as in Gmail attachments (`blocks/blocks/gmail.ts`). Never overload the advanced side with alternate identifiers (URL, provider asset ID) — give those their own subblocks, mark mutually exclusive sources `required: false`, and enforce "exactly one" at execution.
Expand Down
7 changes: 6 additions & 1 deletion .cursor/commands/add-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -866,7 +866,12 @@ export const {service}UploadTool: ToolConfig<Params, Response> = {
fileContent: { type: 'string', required: false, visibility: 'hidden' }, // Legacy
},
request: {
url: '/api/tools/{service}/upload', // Internal route
// Internal route. A static string is a source literal, so the transport trusts it. When the
// path is dynamic, use `internalRoute` from '@/lib/core/utils/internal-route' instead of a template
// string — a builder's plain `/api/...` string is treated as external, because a caller- or
// model-supplied param can produce one:
// url: (params) => internalRoute`/api/tools/{service}/upload/${params.folderId}`
url: '/api/tools/{service}/upload',
method: 'POST',
body: (params) => ({
accessToken: params.accessToken,
Expand Down
31 changes: 31 additions & 0 deletions .cursor/commands/add-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,37 @@ export const {serviceName}{Action}Tool: ToolConfig<
- Always explicitly set `required: true` or `required: false`
- Optional params should have `required: false`

## Internal Routes (calling Sim's own API)

Most tools call a third-party service and `request.url` returns an absolute `https://...` URL. A
tool that instead calls Sim's own API — a `/api/tools/{service}/{action}` proxy route, or a platform
route like `/api/table/...` — must SAY SO, because the transport resolves those against the internal
base URL and signs them with an internal token for the executing user.

That declaration comes from the tool's source, never from the resolved string: a `user-or-llm` param
can make any tool emit `/api/...` (the HTTP Request tool passes its `url` through verbatim, and a
self-hosted integration with a blank host param collapses `${host}/api/v2/x` to `/api/v2/x`).

```typescript
import { internalRoute } from '@/lib/core/utils/internal-route'

// ✓ Static route — a source literal no param can influence
url: '/api/tools/{service}/{action}',

// ✓ Dynamic route — branded, and every interpolated id is percent-encoded for you
url: (params) => internalRoute`/api/table/${params.tableId}/rows`,

// ✓ Query params via withQuery (accepts an object or URLSearchParams; skips undefined/null)
url: (params) => internalRoute`/api/logs`.withQuery({ workspaceId, limit: params.limit }),

// ✗ Treated as EXTERNAL and will fail — a builder's plain string carries no provenance
url: (params) => `/api/table/${params.tableId}/rows`,
```

`internalRoute` throws on a path outside `/api/` and on a query string inside the template. Never
write `encodeURIComponent` inside the template — the tag already encodes each `${...}`, so doing it
yourself double-encodes the value.

## Resolved Secrets and Provenance Boundaries

- Leave ordinary external API inputs and third-party results unchanged. Add provenance handling only
Expand Down
9 changes: 9 additions & 0 deletions .cursor/commands/validate-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ For **every** tool file, check:
- [ ] Tool `description` is a concise one-liner describing what it does
- [ ] Tool `version` is set (`'1.0.0'` or `'2.0.0'` for V2)

### Request URL
- [ ] A tool calling a third-party service returns an ABSOLUTE `https://...` URL
- [ ] A tool calling Sim's own API declares it: a static `/api/...` string, or `internalRoute` from
`@/lib/core/utils/internal-route` when the path is dynamic (query params via `.withQuery({...})`)
- [ ] No builder returns a bare `` `/api/...` `` template string — that is treated as external and
will fail, because a `user-or-llm` param can produce the same string
- [ ] No `encodeURIComponent` inside an `internalRoute` template (the tag already encodes, so this
double-encodes the value)

### Params
- [ ] All required API params are marked `required: true`
- [ ] All optional API params are marked `required: false`
Expand Down
1 change: 1 addition & 0 deletions .cursor/rules/sim-integrations.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The full authoring instructions — tool/block/icon/trigger scaffolding, SubBloc
## Hard rules (don't get these wrong)

- Tool IDs are `snake_case` (`service_action`). Register tools in `tools/registry.ts`, blocks in `blocks/registry-maps.ts` (the `BLOCK_REGISTRY` config map + `BLOCK_META_REGISTRY` catalog-meta map, alphabetically — `blocks/registry.ts` holds only the accessor functions), triggers in `triggers/registry.ts`.
- A tool that calls Sim's own API declares it: a static `request.url` string (`'/api/tools/{service}/{action}'`), or `` internalRoute`/api/table/${params.tableId}/rows` `` from `@/lib/core/utils/internal-route` when the path is dynamic (query params via `.withQuery({...})`). The transport signs internal requests with the executing user's token, so that decision follows the tool's source, never the resolved string — a builder returning a bare `/api/...` string is treated as EXTERNAL, because a `user-or-llm` param can produce one. `internalRoute` encodes every `${...}`; never add `encodeURIComponent` inside the template.
- Type coercions (`Number()`, etc.) belong in `tools.config.params` (runs at execution, after variable resolution) — never in `tools.config.tool` (runs at serialization; coercing there destroys dynamic `<Block.output>` references).
- `canonicalParamId` must NOT match any subblock's `id`, must be unique per operation/condition context, and all subblocks in a canonical group must share the same `required` status. The `inputs` section and the params function reference canonical IDs, not raw subblock IDs.
- Blocks must also set the catalog/UI metadata fields `integrationType`, `tags`, `authMode`, `docsLink`, and export a `{Service}BlockMeta` — see the `/add-block` skill's BlockMeta section for details.
24 changes: 23 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -461,13 +461,35 @@ Use `@sim/testing` mocks/factories over local test data.

New integrations are built in order: **Tools** → **Block** → **Icon** → (optional) **Trigger**. Always look up the service's API docs first.

Two hard rules that the skills assume:
Three hard rules that the skills assume:

- **Tool IDs are `snake_case`** (`service_action`) and must be registered in `tools/registry.ts`; blocks register in `blocks/registry.ts` (alphabetically).
- **`tools.config.tool` runs during serialization (before variable resolution)** — never do `Number()` or other type coercions there, or dynamic references like `<Block.output>` are destroyed. Put all type coercions in `tools.config.params`, which runs during execution after variables resolve.
- **A tool that calls Sim's own API declares it** — a static `request.url` string (`'/api/tools/{service}/{action}'`), or `internalRoute` from `@/lib/core/utils/internal-route` when the path is dynamic. A builder returning a bare `/api/...` string is treated as EXTERNAL and will fail, because params can produce that string. See "Internal tool routes" below.

For the full authoring instructions — SubBlock property tables, `condition`/`dependsOn`/`required`/`mode`/`canonicalParamId` syntax, required block metadata (`integrationType`, `tags`, `authMode`, `docsLink`, `{Service}BlockMeta`), file-input/`normalizeFileInput` patterns, and checklists — use the skills: `/add-integration` (end-to-end), `/add-tools`, `/add-block`, `/add-trigger`.

### Internal tool routes

The transport resolves a tool request against the internal base URL and signs it with an internal token for the executing user. That decision comes from the tool's source, never from the resolved URL string — a `user-or-llm` param can make any tool emit `/api/...` (the HTTP Request tool passes its `url` through verbatim; a self-hosted integration with a blank host param collapses `${host}/api/v2/x` to `/api/v2/x`).

```typescript
// ✓ Static route — a source literal no param can influence
url: '/api/tools/{service}/{action}',

// ✓ Dynamic route — branded, and every interpolated id is encoded for you
import { internalRoute } from '@/lib/core/utils/internal-route'
url: (params) => internalRoute`/api/table/${params.tableId}/rows`,

// ✓ Query params go through withQuery, not the template
url: (params) => internalRoute`/api/logs`.withQuery({ workspaceId, limit: params.limit }),

// ✗ Treated as EXTERNAL — a builder's plain string carries no provenance
url: (params) => `/api/table/${params.tableId}/rows`,
```

`internalRoute` rejects a path outside `/api/`, rejects a query string in the template, and percent-encodes every `${...}` so an id can fill a segment but never widen the path into another route. Never hand-roll `encodeURIComponent` inside the template — that double-encodes.

## Tables

Table column types are registry entries in `apps/sim/lib/table/column-types/` — one file per type owning its label, icon, storage cast, coercion, validation, conversion compatibility, formatting, and editor. `Record<ColumnType, …>` on `registry.ts` and `registry.server.ts` is a compile-time completeness gate: adding a type to the union errors until both entries exist.
Expand Down
Loading
Loading