Agent skill

Add Connector

by simstudioai in simstudioai/sim

Add or update a Sim knowledge base connector for syncing documents from an external source, including auth mode, config fields, pagination, document mapping, tags, and registry wiring.

Apache-2.0Auto-check passedKnowledge Management

Install Add Connector

skills CLI
$ npx skills add simstudioai/sim --skill add-connector -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install simstudioai/sim add-connector --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/simstudioai/sim.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.agents/skills/add-connector .claude/skills/add-connector && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
add-connector
GitHub stars
30k
Token cost
~7.8k tokens
SKILL.md length
2,558 words
Files
2
Skills in repo
40
Repo updated
First seen
Licence
Apache-2.0

At a glance

Add or update a Sim knowledge base connector for syncing documents from an external source, including auth mode, config fields, pagination, document mapping, tags, and registry wiring.

  • Works in 4 steps: Use Context7 or WebFetch to read the… → Determine the auth mode: OAuth (if Sim… → Create the connector directory: a… → …
  • Working in apps/sim/connectors/{service}/
  • SKILL.md covers Choose the connector runtime…, Your Task, Hard Rule: No Guessed Response… and Directory Structure, plus 10 more sections
  • Reaches service.com

What it does

Add Connector is an agent skill from simstudioai/sim. Add or update a Sim knowledge base connector for syncing documents from an external source, including auth mode, config fields, pagination, document mapping, tags, and registry wiring. Use when working in apps/sim/connectors/{service}/ or adding a new external document source.

Its SKILL.md is about 7.8k tokens, which your agent loads only when the skill is triggered. The skill folder holds 2 other files (for example `agents/openai.yaml`).

It sits in Knowledge Management, covering Knowledge bases. The repository describes itself as: Sim is the collaborative workspace to build, deploy, and monitor AI agents and workflows. Used by 100,000+ builders. The licence is Apache-2.0.

When your agent uses it

  • Working in apps/sim/connectors/{service}/
  • Adding a new external document source

Example prompts

  • “/add-connector”

Workflow steps

4 steps, taken from the first numbered list in SKILL.md.

  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…
  3. Create the connector directory: a client-safe meta.ts (declarative metadata) plus the runtime module that spreads it
  4. Register it in BOTH the server registry and the client-safe meta registry

What it can do on your machine

Read from SKILL.md and the folder at commit 546d4e7. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    No scripts in the folder and no shell commands in SKILL.md (its code samples are typescript).

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    Hosts in commands or code, which the agent is likely to contact:

    • service.com

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Add Connector loads about 7.8k tokens when it runs. Until then it costs about 73 tokens; SKILL.md has 2,558 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~73
When it runs · the whole SKILL.md, loaded when a task matches
~7.8k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from simstudioai/sim at commit 546d4e7, republished under its Apache-2.0 licence (© simstudioai). 2,558 words, ~7,794 tokens.

Download SKILL.mdSave it as .claude/skills/add-connector/SKILL.md (or your agent's skills folder). This skill also uses 1 other file; get the full folder from GitHub.
name
add-connector
description
Add or update a Sim knowledge base connector for syncing documents from an external source, including auth mode, config fields, pagination, document mapping, tags, and registry wiring. Use when working in `apps/sim/connectors/{service}/` or adding a new external document source.
argument-hint
<service-name> [api-docs-url]

Add Connector Skill

Choose the connector runtime first

For Sim Search, use the live provider workflow in the federated Search developer guide. Its browser-safe provider catalog owns provider IDs, API origins, credential aliases, and account modes; its typed runtime registry requires both search and read handlers. ConnectorMeta remains the owner of logos and setup fields. Member mode has no admin resource filters. Service mode requires independent live source verification; resource pickers use shared selectors with canonical manual-input pairs, and plain inputs are fine where no picker applies. Do not implement a Search source by adding a crawler, embeddings, or a scheduled ACL build.

The ingestion instructions below apply to ordinary knowledge-base connectors. Sim Search always uses the live backend, so setting search: true in metadata does not implement federated search; a provider that serves both needs the live handlers and this KB ingestion path.

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: a client-safe meta.ts (declarative metadata) plus the runtime module that spreads it
  4. Register it in BOTH the server registry and the client-safe meta registry

Hard Rule: No Guessed Response Or Document Schemas

If the service docs do not clearly show the document list response, document fetch response, pagination shape, or metadata fields, you MUST tell the user instead of guessing.

  • Do NOT invent document fields
  • Do NOT guess pagination cursors or next-page fields
  • Do NOT infer metadata/tag mappings from unrelated endpoints
  • Do NOT fabricate ExternalDocument content structure from partial docs

If the source schema is unknown, do one of these instead:

  1. Ask the user for sample API responses
  2. Ask the user for test credentials so you can verify live payloads
  3. Implement only the documented parts of the connector
  4. Leave the connector incomplete and explicitly say which fields remain unknown

Directory Structure

Each connector is split into a client-safe metadata file and a server-only runtime file. This mirrors the XBlockMeta / BLOCK_META_REGISTRY split in apps/sim/blocks — client components (the knowledge UI) only need the metadata (icon, name, auth, config fields), so the runtime functions (which pull server-only helpers like input-validation.server → undici → node:net) must stay out of the client bundle.

Create files in apps/sim/connectors/{service}/:

connectors/{service}/
├── index.ts          # Barrel export (re-exports the runtime connector)
├── meta.ts           # ConnectorMeta — client-safe declarative metadata
└── {service}.ts      # ConnectorConfig — spreads the meta + adds runtime functions
  • meta.ts exports {service}ConnectorMeta: ConnectorMeta. It imports ONLY the icon from @/components/icons, import type { ConnectorMeta } from '@/connectors/types', and any pure-data constants. It must NEVER import server/runtime code.
  • {service}.ts exports {service}Connector: ConnectorConfig. It imports the meta via import { {service}ConnectorMeta } from '@/connectors/{service}/meta', spreads it as the first property, and holds the runtime functions (which may import server-only helpers like @/lib/knowledge/documents/utils).

Authentication

Connectors use a discriminated union for auth config (ConnectorAuthConfig in connectors/types.ts):

See ConnectorAuthConfig in apps/sim/connectors/types.ts: oauth takes provider, requiredScopes, and optional service-account/admin scopes; apiKey takes label, placeholder, and optional.

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.

Connector Structure (meta.ts + runtime)

The declarative metadata lives in meta.ts (ConnectorMeta). The runtime functions live in {service}.ts (ConnectorConfig), which spreads the meta as its first property.

meta.ts — client-safe metadata
typescript
import { {Service}Icon } from '@/components/icons'
import type { ConnectorMeta } from '@/connectors/types'

export const {service}ConnectorMeta: ConnectorMeta = {
  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', 'dropdown', and 'selector' types — see ConfigField Types below
  ],

  // Optional: tag definitions are metadata too — declare them here
  // tagDefinitions: [ ... ],
}

Keep meta.ts free of any server/runtime import. Only the icon, the ConnectorMeta type, and pure-data constants belong here.

{service}.ts — runtime (OAuth example)
typescript
import { createLogger } from '@sim/logger'
import { fetchWithRetry } from '@/lib/knowledge/documents/secure-fetch.server'
import { {service}ConnectorMeta } from '@/connectors/{service}/meta'
import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types'

const logger = createLogger('{Service}Connector')

export const {service}Connector: ConnectorConfig = {
  ...{service}ConnectorMeta,

  listDocuments: async (accessToken, sourceConfig, cursor) => {
    // Return metadata stubs with contentDeferred: true (if per-doc content fetch needed)
    // Or full documents with content (if list API returns content inline)
    // Return { documents: ExternalDocument[], nextCursor?, hasMore }
  },

  getDocument: async (accessToken, sourceConfig, externalId) => {
    // Fetch full content for a single document
    // Return ExternalDocument with contentDeferred: false, or null
  },

  validateConfig: async (accessToken, sourceConfig) => {
    // Return { valid: true } or { valid: false, error: 'message' }
  },

  // Optional: map source metadata to semantic tag keys (translated to slots by sync engine)
  mapTags: (metadata) => {
    // Return Record<string, unknown> with keys matching tagDefinitions[].id
  },
}

Only map fields in listDocuments, getDocument, validateConfig, and mapTags when the source payload shape is documented or live-verified. If not, tell the user and stop rather than guessing.

API key connector example

The split is identical — auth lives in meta.ts, runtime functions in {service}.ts.

typescript
// meta.ts
export const {service}ConnectorMeta: ConnectorMeta = {
  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: [ /* ... */ ],
}

// {service}.ts
export const {service}Connector: ConnectorConfig = {
  ...{service}ConnectorMeta,
  listDocuments: async (accessToken, sourceConfig, cursor) => { /* ... */ },
  getDocument: async (accessToken, sourceConfig, externalId) => { /* ... */ },
  validateConfig: async (accessToken, sourceConfig) => { /* ... */ },
}

ConfigField Types

The add-connector modal renders these automatically — no custom UI needed.

Three field types are supported: short-input, dropdown, and selector.

typescript
// Text input
{
  id: 'domain',
  title: 'Domain',
  type: 'short-input',
  placeholder: 'yoursite.example.com',
  required: true,
}

// Dropdown (static options)
{
  id: 'contentType',
  title: 'Content Type',
  type: 'dropdown',
  required: false,
  options: [
    { label: 'Pages only', id: 'page' },
    { label: 'Blog posts only', id: 'blogpost' },
    { label: 'All content', id: 'all' },
  ],
}

Dynamic Selectors (Canonical Pairs)

Use type: 'selector' for a key declared in the browser-safe selector manifest at apps/sim/lib/selectors/manifest.ts. Remote selectors execute through the authorized selectors.execute server operation and a server attachment; connectors never call providers or resolve credentials in the browser. Apply the add-selector skill when the key does not exist.

Selectors are paired with a manual fallback input using the canonical pair pattern — a selector field (basic mode) and a short-input field (advanced mode) linked by canonicalParamId.

The user sees a toggle button (ArrowLeftRight) to switch between the selector dropdown and manual text input. On submit, the modal resolves each canonical pair to the active mode's value, keyed by canonicalParamId.

Rules
  1. Every selector field MUST have a canonical pair — a corresponding short-input (or dropdown) field with the same canonicalParamId and mode: 'advanced'.
  2. required must be set identically on both fields in a pair. If the selector is required, the manual input must also be required.
  3. canonicalParamId must match the key the connector expects in sourceConfig (e.g. baseId, channel, teamId). The advanced field's id should typically match canonicalParamId (connector config fields differ from block subBlocks here; the block rule that canonicalParamId must not equal the id of a subblock without a canonicalParamId does not apply).
  4. dependsOn references the selector field's id, not the canonicalParamId. The modal propagates dependency clearing across canonical siblings automatically — changing either field in a parent pair clears dependent children.
Selector canonical pair example (Airtable base → table cascade)
typescript
configFields: [
  // Base: selector (basic) + manual (advanced)
  {
    id: 'baseSelector',
    title: 'Base',
    type: 'selector',
    selectorKey: 'airtable.bases',     // Must exist in lib/selectors/manifest.ts
    canonicalParamId: 'baseId',
    mode: 'basic',
    placeholder: 'Select a base',
    required: true,
  },
  {
    id: 'baseId',
    title: 'Base ID',
    type: 'short-input',
    canonicalParamId: 'baseId',
    mode: 'advanced',
    placeholder: 'e.g. appXXXXXXXXXXXXXX',
    required: true,
  },
  // Table: selector depends on base (basic) + manual (advanced)
  {
    id: 'tableSelector',
    title: 'Table',
    type: 'selector',
    selectorKey: 'airtable.tables',
    canonicalParamId: 'tableIdOrName',
    mode: 'basic',
    dependsOn: ['baseSelector'],       // References the selector field ID
    placeholder: 'Select a table',
    required: true,
  },
  {
    id: 'tableIdOrName',
    title: 'Table Name or ID',
    type: 'short-input',
    canonicalParamId: 'tableIdOrName',
    mode: 'advanced',
    placeholder: 'e.g. Tasks',
    required: true,
  },
  // Non-selector fields stay as-is
  { id: 'maxRecords', title: 'Max Records', type: 'short-input', ... },
]
Selector with domain dependency (Jira/Confluence pattern)

When a selector depends on a plain short-input field (no canonical pair), dependsOn references that field's id directly. Exact references such as {{JIRA_DOMAIN}} remain unresolved in the browser and are resolved only after workspace authorization on the server.

typescript
configFields: [
  {
    id: 'domain',
    title: 'Jira Domain',
    type: 'short-input',
    placeholder: 'yoursite.atlassian.net',
    required: true,
  },
  {
    id: 'projectSelector',
    title: 'Project',
    type: 'selector',
    selectorKey: 'jira.projects',
    canonicalParamId: 'projectKey',
    mode: 'basic',
    dependsOn: ['domain'],
    placeholder: 'Select a project',
    required: true,
  },
  {
    id: 'projectKey',
    title: 'Project Key',
    type: 'short-input',
    canonicalParamId: 'projectKey',
    mode: 'advanced',
    placeholder: 'e.g. ENG, PROJ',
    required: true,
  },
]
How dependsOn maps to SelectorContext

The shared connector context builder projects only active dependencies. A canonical dependency uses its active basic or advanced value under canonicalParamId; a non-canonical dependency uses its field id. The resulting key must be a SelectorContextKey in apps/sim/lib/selectors/types.ts and must be explicitly allowed by that selector's manifest entry. The browser sends the connector's workspace scope, not the complete connector configuration.

Available selector keys

Check apps/sim/lib/selectors/manifest.ts for the exhaustive selector keys. Common ones for connectors:

SelectorKeyContext DepsReturns
airtable.basescredentialBase ID + name
airtable.tablescredential, baseIdTable ID + name
slack.channelscredentialChannel ID + name
gmail.labelscredentialLabel ID + name
google.calendarcredentialCalendar ID + name
linear.teamscredentialTeam ID + name
linear.projectscredential, teamIdProject ID + name
jira.projectscredential, domainProject key + name
confluence.spacescredential, domainSpace key + name
notion.databasescredentialDatabase ID + name
asana.workspacescredentialWorkspace GID + name
microsoft.teamscredentialTeam ID + name
microsoft.channelscredential, teamIdChannel ID + name
webflow.sitescredentialSite ID + name
outlook.folderscredentialFolder ID + name

ExternalDocument Shape

Every document returned from listDocuments/getDocument must include:

typescript
{
  externalId: string          // Source-specific unique ID
  title: string               // Document title
  content: string             // Extracted plain text (or '' if contentDeferred)
  contentDeferred?: boolean   // true = content will be fetched via getDocument
  mimeType: 'text/plain'     // extracted text; for a format the KB pipeline parses (PDF, Office), set `sourceFile` instead
  contentHash: string         // Change-detection hash (metadata-based when content is deferred)
  sourceUrl?: string          // Link back to original (stored on document record)
  metadata?: Record<string, unknown>  // Source-specific data (fed to mapTags)
}

Content Deferral (Required for file/content-download connectors)

All connectors that require per-document API calls to fetch content MUST use contentDeferred: true. This is the standard pattern — listDocuments returns lightweight metadata stubs, and content is fetched lazily by the sync engine via getDocument only for new/changed documents.

This pattern is critical for reliability: the sync engine processes documents in batches and enqueues each batch for processing immediately. If a sync times out, all previously-batched documents are already queued. Without deferral, content downloads during listing can exhaust the sync task's time budget before any documents are saved.

When to use contentDeferred: true
  • The service's list API does NOT return document content (only metadata)
  • Content requires a separate download/export API call per document
  • Examples: Google Drive, OneDrive, SharePoint, Dropbox, Notion, Confluence, Gmail, Obsidian, Evernote, GitHub
When NOT to use contentDeferred
  • The list API already returns the full content inline (e.g., Slack messages, Reddit posts, HubSpot notes)
  • No per-document API call is needed to get content
Content Hash Strategy

Deferred-content connectors (contentDeferred: true) must use a metadata-based contentHash derivable from the list response alone, so the sync engine can detect changes without downloading content. Inline-content connectors may hash the content they already hold (computeContentHash).

Good metadata hash sources:

  • modifiedTime / lastModifiedDateTime — changes when file is edited
  • Git blob SHA — unique per content version
  • API-provided content hash (e.g., Dropbox content_hash)
  • Version number (e.g., Confluence page version)

Format: {service}:{id}:{changeIndicator}

typescript
// Google Drive: modifiedTime changes on edit
contentHash: `gdrive:${file.id}:${file.modifiedTime ?? ''}`

// GitHub: blob SHA is a content-addressable hash
contentHash: `gitsha:${item.sha}`

// Dropbox: API provides content_hash
contentHash: `dropbox:${entry.id}:${entry.content_hash ?? entry.server_modified}`

// Confluence: version number increments on edit
contentHash: `confluence:${page.id}:${page.version.number}`

Critical invariant: The contentHash MUST be identical whether produced by listDocuments (stub) or getDocument (full doc). Both should use the same stub function to guarantee this.

Implementation Pattern
typescript
// 1. Create a stub function (sync, no API calls)
function fileToStub(file: ServiceFile): ExternalDocument {
  return {
    externalId: file.id,
    title: file.name || 'Untitled',
    content: '',
    contentDeferred: true,
    mimeType: 'text/plain',
    sourceUrl: `https://service.com/file/${file.id}`,
    contentHash: `service:${file.id}:${file.modifiedTime ?? ''}`,
    metadata: { /* fields needed by mapTags */ },
  }
}

// 2. listDocuments returns stubs (fast, metadata only)
listDocuments: async (accessToken, sourceConfig, cursor) => {
  const response = await fetchWithRetry(listUrl, { ... })
  const files = (await response.json()).files
  const documents = files.map(fileToStub)
  return { documents, nextCursor, hasMore }
}

// 3. getDocument fetches content and returns full doc with SAME contentHash
getDocument: async (accessToken, sourceConfig, externalId) => {
  const metadata = await fetchWithRetry(metadataUrl, { ... })
  const file = await metadata.json()
  if (file.trashed) return null

  try {
    const content = await fetchContent(accessToken, file)
    if (!content.trim()) return null
    const stub = fileToStub(file)
    return { ...stub, content, contentDeferred: false }
  } catch (error) {
    logger.warn(`Failed to fetch content for: ${file.name}`, { error })
    return null
  }
}
Reference Implementations
  • Google Drive: connectors/google-drive/google-drive.ts — file download/export with modifiedTime hash
  • GitHub: connectors/github/github.ts — git blob SHA hash
  • Notion: connectors/notion/notion.ts — blocks API with last_edited_time hash
  • Confluence: connectors/confluence/confluence.ts — version number hash

tagDefinitions — Declared Tag Definitions

Declare which tags the connector populates using semantic IDs. Shown in the add-connector modal as opt-out checkboxes. On connector creation, slots are dynamically assigned via getNextAvailableSlot — connectors never hardcode slot names.

typescript
tagDefinitions: [
  { id: 'labels', displayName: 'Labels', fieldType: 'text' },
  { id: 'version', displayName: 'Version', fieldType: 'number' },
  { id: 'lastModified', displayName: 'Last Modified', fieldType: 'date' },
],

Each entry has:

  • id: Semantic key matching a key returned by mapTags (e.g. 'labels', 'version')
  • displayName: Human-readable name shown in the UI (e.g. "Labels", "Last Modified")
  • fieldType: 'text' | 'number' | 'date' | 'boolean' — determines which slot pool to draw from

Users can opt out of specific tags in the modal. Disabled IDs are stored in sourceConfig.disabledTagIds. The assigned mapping (semantic id → slot) is stored in sourceConfig.tagSlotMapping.

Show full SKILL.md (989 more words)Show less

@/connectors/utils Helpers

Reuse these instead of inlining the same logic:

  • htmlToPlainText(html) — strip HTML to plain text before indexing ExternalDocument.content. Never index raw HTML.
  • computeContentHash(content) — stable content hash for change detection.
  • parseTagDate(value) — parse to a valid Date or undefined (guards Invalid Date). Use in mapTags for date fields.
  • joinTagArray(value) — validate an array and join to a comma-separated string, or undefined. Use in mapTags for array/label fields.
  • parseMultiValue(value) — normalize a value into a string[].

mapTags — Metadata to Semantic Keys

Maps source metadata to semantic tag keys. Required if tagDefinitions is set. 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.

Use the @/connectors/utils helpers for the common transforms — don't hand-roll date/array validation:

typescript
import { joinTagArray, parseTagDate } from '@/connectors/utils'

mapTags: (metadata: Record<string, unknown>): Record<string, unknown> => {
  const result: Record<string, unknown> = {}

  // joinTagArray validates the array and joins to a comma-separated string (undefined if empty)
  const labels = joinTagArray(metadata.labels)
  if (labels) result.labels = labels

  // Validate numbers — guard against NaN
  if (metadata.version != null) {
    const num = Number(metadata.version)
    if (!Number.isNaN(num)) result.version = num
  }

  // parseTagDate returns a valid Date or undefined (guards against Invalid Date)
  const lastModified = parseTagDate(metadata.lastModified)
  if (lastModified) result.lastModified = lastModified

  return result
}

External API Calls — Use fetchWithRetry

All external API calls must use fetchWithRetry from @/lib/knowledge/documents/secure-fetch.server instead of raw fetch(). It does not validate the host (on a direct outbound route it calls plain fetch), so use secureFetchWithRetry for user-controlled hosts. 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 within a 150s budget).

typescript
import { fetchWithRetry } from '@/lib/knowledge/documents/secure-fetch.server'
import { VALIDATE_RETRY_OPTIONS } 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).

Capped or Incomplete Listings — syncContext.listingCapped (REQUIRED)

If listDocuments can ever return less than the full source set on a non-incremental sync — a maxItems/maxDocuments-style cap, or a transient per-item error that drops a still-existing document from the listing — it MUST set syncContext.listingCapped = true when that happens.

The engine reconciles deletions only when the listing is marked safe (checkpoint.unsafe in lib/knowledge/connectors/listing-checkpoint.ts): syncContext.listingCapped, syncContext.listingTruncated, syncContext.reconciliationUnsafe, ExternalDocumentList.reconciliationSafe: false (required for offset/unstable pagination), or a non-null listingFailures marks it unsafe. Anything absent from a safe listing is tombstoned on that sync and hard-deleted when the next sync still does not see it — so a truncated listing without this flag eventually removes every real document beyond the cap.

typescript
if (hitLimit && syncContext) {
  syncContext.listingCapped = true
}

Rules:

  • Set it when a user-configured cap truncates the listing while more documents exist
  • Set it when a thrown error caused a still-present document to be skipped during listing
  • Do NOT set it when the source is genuinely exhausted (deleted documents must still reconcile)
  • Do NOT set it for intentional scope filters (e.g. a date cutoff) — out-of-scope documents should be reconciled normally

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. Tombstones documents absent from a reconciliation-safe listing and hard-deletes them when the next safe listing still omits them
  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 for connector-sourced documents in the knowledge base table. The icon is read from CONNECTOR_META_REGISTRY[connectorType].icon (the client-safe registry) 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

Register in BOTH registries, keeping the same alphabetical-by-id ordering in each.

  1. Server registry — apps/sim/connectors/registry.server.ts (server-only full registry; holds full connectors with runtime functions, imported by the sync engine and knowledge API routes):
typescript
import { {service}Connector } from '@/connectors/{service}'

export const CONNECTOR_REGISTRY: ConnectorRegistry = {
  // ... existing connectors ...
  {service}: {service}Connector,
}
  1. Client-safe meta registry — apps/sim/connectors/registry.ts (imports each connector's meta.ts only, so client components can use it without pulling server-only code; the metadata counterpart to BLOCK_META_REGISTRY):
typescript
import { {service}ConnectorMeta } from '@/connectors/{service}/meta'

export const CONNECTOR_META_REGISTRY: ConnectorMetaRegistry = {
  // ... existing connector metas ...
  {service}: {service}ConnectorMeta,
}

registry.ts exports CONNECTOR_META_REGISTRY: ConnectorMetaRegistry plus the helpers getConnectorMeta(id) and getAllConnectorMeta(), importing each @/connectors/{service}/meta directly — never the runtime module. registry.server.ts exports CONNECTOR_REGISTRY: ConnectorRegistry.

Reference Implementations

  • OAuth + contentDeferred: apps/sim/connectors/google-drive/google-drive.ts — file download with metadata-based hash, orderBy for deterministic pagination
  • OAuth + contentDeferred (blocks API): apps/sim/connectors/notion/notion.ts — complex block content extraction deferred to getDocument
  • OAuth + contentDeferred (git): apps/sim/connectors/github/github.ts — blob SHA hash, tree listing
  • OAuth + inline content: apps/sim/connectors/airtable/airtable.ts — list API returns record fields inline; listDocuments and getDocument share recordToDocument, which hashes that content
  • OAuth + contentDeferred + config fields: 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}/meta.ts with {service}ConnectorMeta: ConnectorMeta (icon, name, auth, configFields, tagDefinitions) — no server/runtime imports
  • Created connectors/{service}/{service}.ts with {service}Connector: ConnectorConfig spreading the meta + runtime functions
  • Created connectors/{service}/index.ts barrel export
  • Auth configured correctly:
    • OAuth: auth.provider matches an existing OAuthService in lib/oauth/types.ts
    • API key: auth.label and auth.placeholder set appropriately
  • Selector fields configured correctly (if applicable):
    • Every type: 'selector' field has a canonical pair (short-input or dropdown with same canonicalParamId and mode: 'advanced')
    • required is identical on both fields in each canonical pair
    • selectorKey exists in apps/sim/lib/selectors/manifest.ts
    • dependsOn references selector field IDs (not canonicalParamId)
    • Each projected dependency key is a SelectorContextKey allowed by the selector manifest
    • Validate the selector key itself with the validate-selector skill
  • listDocuments handles pagination; deferred-content connectors use metadata-based content hashes
  • syncContext.listingCapped = true set whenever the listing is truncated (max-items cap or transient per-item error) — required to prevent the engine's deletion reconciliation from removing unseen documents
  • contentDeferred: true used if content requires per-doc API calls (file download, export, blocks fetch)
  • contentHash is metadata-based for deferred-content connectors (inline-content ones may use computeContentHash) and identical between stub and getDocument
  • sourceUrl set on each ExternalDocument (full URL, not relative)
  • metadata includes source-specific data for tag mapping
  • tagDefinitions declared for each semantic key returned by mapTags
  • mapTags implemented if source has useful metadata (labels, dates, versions)
  • validateConfig verifies the source is accessible
  • All external API calls use fetchWithRetry (not raw fetch)
  • All optional config fields validated in validateConfig
  • Icon exists in components/icons.tsx (or asked user to provide SVG)
  • Registered the full connector in connectors/registry.server.ts
  • Registered the meta in connectors/registry.ts (same alphabetical-by-id ordering as registry.server.ts)

© simstudioai, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

SKILL.md and 1 other file in .agents/skills/add-connector of simstudioai/sim.

  • SKILL.md
  • agents/openai.yaml

Open the folder on GitHubat commit 546d4e7

Compare with similar skills

Add Connector next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Add Connector compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Add Connector this skillsimstudioai/sim30k—~7.8kAutomated safety check: PassApache-2.0
Ima Knowledge Basecountbot-ai/CountBot781—~506Automated safety check: PassMIT
Wiki Viewerrohitg00/pro-workflow2.9k—~1.1kAutomated safety check: PassNone
HypatiaMarchLiu/hypatia239—~7.9kAutomated safety check: NotesMIT
Psr Autoloading Knowledgedykyi-roman/awesome-claude-code103—~2.1kAutomated safety check: PassMIT
Psr Overview Knowledgedykyi-roman/awesome-claude-code103—~2.7kAutomated safety check: PassMIT

Similar skills

  • Ima Knowledge Base

    countbot-ai/CountBot

    通过 IMA OpenAPI 处理知识库任务。支持知识库内容搜索、命中详情查看、条目浏览、列出知识库、上传文件、导入网页。用户提到知识库、资料库、上传到知识库、导入网页、搜知识库时使用。

    781 GitHub stars~506 tokensUpdated 3 days ago
    Knowledge ManagementAuto-check passed
  • Wiki Viewer

    rohitg00/pro-workflow

    Render a self-contained HTML viewer for a pro-workflow wiki.

    2.9k GitHub stars~1.1k tokensUpdated 8 days ago
    Knowledge ManagementAuto-check passed
  • Hypatia

    MarchLiu/hypatia

    Interact with the Hypatia AI memory system using natural language.

    239 GitHub stars~7.9k tokensUpdated 8 days ago
    Knowledge ManagementAuto-check: notes
  • Psr Autoloading Knowledge

    dykyi-roman/awesome-claude-code

    PSR-4 autoloading standard knowledge base for PHP 8.4 projects.

    103 GitHub stars~2.1k tokensUpdated 1 mo ago
    Knowledge ManagementAuto-check passed
  • Psr Overview Knowledge

    dykyi-roman/awesome-claude-code

    PHP Standards Recommendations (PSR) overview knowledge base.

    103 GitHub stars~2.7k tokensUpdated 1 mo ago
    Knowledge ManagementAuto-check passed
  • Siyuan

    Tommy-yw/RunbookHermes

    SiYuan Note API for searching, reading, creating, and managing blocks and documents in a self-hosted knowledge base via curl.

    546 GitHub starsUsed in 3 repos~2.3k tokens
    Knowledge ManagementAuto-check: notes

More from simstudioai/sim

All 40 skills in this repo
  • Sim Helm

    simstudioai/sim

    Install, upgrade, and operate the Sim Helm chart on Kubernetes.

    30k GitHub stars~2.2k tokensUpdated today
    Auto-check passed
  • Add Column Type

    simstudioai/sim

    Add a new table column type to Sim — registry entry, icon, storage shape, coercion, and the behavioral hooks the grid and API read.

    30k GitHub stars~2.9k tokensUpdated today
    Auto-check passed
  • Add Enrichment

    simstudioai/sim

    Add a code-defined table enrichment (registry entry) under apps/sim/enrichments/ backed by an ordered provider cascade, ensuring every provider tool it calls has hosted-key support.

    30k GitHub stars~2.2k tokensUpdated today
    Auto-check passed
  • Add Hosted Key

    simstudioai/sim

    Add hosted API key support to a tool so Sim provides the key (metered and billed to the workspace) when a user has not brought their own.

    30k GitHub stars~3.4k tokensUpdated today
    Auto-check passed
  • Add Managed CLI

    simstudioai/sim

    Add or upgrade a curated, immutable managed CLI for Sim Function sandboxes, including client-safe catalog metadata, a pinned server-only installation recipe, checksum and executable verification…

    30k GitHub stars~2.4k tokensUpdated today
    Auto-check passed
  • Add Selector

    simstudioai/sim

    Add or update a Sim dynamic selector using the shared manifest, server attachment, and selectors.execute path.

    30k GitHub stars~1.7k tokensUpdated today
    Auto-check passed

Questions about Add Connector

What does Add Connector do?

Add or update a Sim knowledge base connector for syncing documents from an external source, including auth mode, config fields, pagination, document mapping, tags, and registry wiring. Add Connector is an agent skill from simstudioai/sim. Add or update a Sim knowledge base connector for syncing documents from an external source, including auth mode, config fields, pagination, document mapping, tags, and registry wiring.

When should I use Add Connector?

Add Connector fits situations like: working in apps/sim/connectors/{service}/; adding a new external document source.

How do I install Add Connector in Claude Code?

Run `npx skills add simstudioai/sim --skill add-connector -a claude-code`. Or copy the skill folder (.agents/skills/add-connector in simstudioai/sim) into .claude/skills/add-connector in your project. Claude Code loads it when a task matches its description.

How do I install Add Connector in Codex?

Run `npx skills add simstudioai/sim --skill add-connector -a codex`. Or copy the skill folder (.agents/skills/add-connector in simstudioai/sim) into .agents/skills/add-connector in your project. Codex loads it when a task matches its description.

Can I use Add Connector in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add simstudioai/sim --skill add-connector -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/add-connector, .gemini/skills/add-connector, .github/skills/add-connector and .opencode/skills/add-connector in your project.

What does Add Connector need to run?

SKILL.md names no scripts, command-line tools or credentials: Add Connector is instructions for the agent only.

Does Add Connector access the network?

SKILL.md names 1 domain. In commands or code: service.com; the agent is likely to contact it when it follows the instructions. This is read from the text; nothing was executed.

Is Add Connector safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Add Connector use?

Add Connector is published under the Apache-2.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Add Connector use?

About 7.8k tokens (SKILL.md is roughly 31k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Add Connector?

Skills that share tags, products or a category with Add Connector: Ima Knowledge Base (countbot-ai/CountBot, 781 stars), Wiki Viewer (rohitg00/pro-workflow, 2.9k stars), Hypatia (MarchLiu/hypatia, 239 stars) and Psr Autoloading Knowledge (dykyi-roman/awesome-claude-code, 103 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Add Connector?

simstudioai (a GitHub organization) maintains it in simstudioai/sim, which has 29,785 GitHub stars. The repository holds 40 skills in this directory. The repository was last updated on October 7, 2026.

Source: simstudioai/sim on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.