Connector reference
This page defines the connector contract: manifest fields, permissions, configuration schema, SDK lifecycle, document fields, state, local validation, and publishing. For a guided introduction, start with Build a connector.
Manifest
manifest.json declares the connector to Gety. Gety reads the manifest when the user installs the connector and keeps that copy. To change the manifest of an installed connector, reinstall it from its local folder, or publish a new version.
| Field | Required | Description |
|---|---|---|
id | Yes | Stable connector ID. Must match ^[a-z0-9][a-z0-9_-]*$: start with a lowercase letter or digit, then use lowercase letters, digits, _, or -. Do not use uppercase letters, dots, or a leading _ or -. Gety identifies an installed connector by its ID, so never change it after you publish. |
name | Yes | Display name shown in Gety's connector list and install dialog. |
version | Yes | SemVer version of the connector. |
min_gety_app_version | Yes | Minimum Gety app version, as SemVer. Older Gety versions refuse to install the connector. Set 0.7.0 or later when the manifest uses permissions or sync_version. |
entry | Yes | Relative path to the built entry inside the connector root, usually dist/main.js. |
capabilities | Yes | Non-empty array of host integration points. A polling data source declares ["data-source"], currently the only supported value. Unknown values are rejected. |
description | No | Short description shown in the install dialog. |
author | No | Author object with optional name, email, and url, for example { "name": "Gety" }. |
icon | No | Relative path to a PNG or SVG in the connector root. Gety shows it in the connector list and install dialog. |
schedule | No | Sync schedule. If omitted, Gety syncs every 600 seconds. See Schedule. |
config | No | Configuration form schema. See Config fields. |
doc_link | No | Declares how documents open. See Doc link. |
schema.extra_gety_indexed_fields | No | Makes extra metadata keys searchable. See Indexed fields. |
permissions | No | What the connector may do. An object runs the connector in a sandbox; omitted or "unrestricted" runs it with full access. Can't be null. See Permissions. |
sync_version | No | Positive integer, 1 if omitted. Raise it when a new version can't continue from what an older version synced. See Versions and updates. |
entry and icon must stay inside the connector root. Unknown manifest fields are rejected.
Schedule
schedule controls how Gety polls the source. Users can change it after installation.
| Shape | Behavior |
|---|---|
{ "strategy": "manual" } | Gety doesn't poll automatically. Users can still sync with More actions (⋯) → Sync now. |
{ "strategy": "interval", "interval_seconds": 3600 } | Poll every interval_seconds. Values below 60 are clamped to 60. |
{ "strategy": "daily", "time": "09:00" } | Poll once per day at time, using local time. HH:mm and HH:mm:ss are supported. |
Config fields
config.fields is an ordered array. Each field renders an input in Gety's install and configuration UI, and becomes a typed property on this.config.
| Property | Required | Description |
|---|---|---|
id | Yes | Field key. It becomes a property on the generated ManifestConfig type. |
title | Yes | Label shown in the UI. |
type | Yes | Input type. One of text, password, number, checkbox, dropdown, or directory. |
description | No | Helper text shown under the field. |
placeholder | No | Placeholder text for text-like inputs. |
required | No | Whether the field must be filled in. Defaults to false. |
default_value | No | Pre-filled value when the field is empty. The value must match the field type; a number field's default must be an integer, so 0.5 is rejected. |
options | For dropdown | Ordered array of { "value": "...", "label": "..." }. |
Field types:
| Type | Use for |
|---|---|
text | Single-line text, such as a URL or username. |
password | API keys and tokens. Values are masked in the UI. |
number | Numeric values. |
checkbox | Boolean on/off options. |
dropdown | A fixed choice from ordered options. |
directory | A local folder path chosen with a picker. |
Use type: "password" for API keys and tokens. For local validation, the sample runner maps field IDs to environment variables. It uppercases the field path and tries the GETY_CONFIG_ prefix first, then the bare name:
api_keyreadsGETY_CONFIG_API_KEYfirst, thenAPI_KEY.auth.api_keyreadsGETY_CONFIG_AUTH_API_KEYfirst, thenAUTH_API_KEY.
Doc link
doc_link declares once, for the whole connector, how documents open.
{ "kind": "url", "field": "url" }
| Property | Description |
|---|---|
kind | url opens a browser link; file opens a local filesystem path. |
field | The doc field or metadata key to read the link value from. |
Resolution rules:
fieldis either a core doc field (id,parent_id,title,content, ordoc_type) or a direct metadata key."field": "url"readsmetadata.url. Use the direct key, not a dot path like"metadata.url".- Missing, empty, or non-string values mean the document has no link.
urllinks open in the browser. Gety doesn't fetch their content.filelinks must be stable local paths, not temporary cache files.
Open, display, and copy actions come from doc_link, not from content_format. content_format only controls how Gety renders the text preview.
For the user's safety, Gety refuses to open a link that points to:
- A file inside any connector's folders, including the connector's own data folder and code. Link to the source service or to the user's own files instead.
- A file that doesn't exist.
- A
file:URL that names a host instead of a local path, such asfile://server/share/doc.txt. - On Windows, a file that isn't on a local volume.
This applies to every link that resolves to a local file, whether kind is file or url. Open file directory on a file link isn't affected.
Indexed fields
Gety always indexes title and content. schema.extra_gety_indexed_fields makes additional metadata keys searchable. Each entry needs both field and strategy:
{
"schema": {
"extra_gety_indexed_fields": [
{ "field": "status", "strategy": "fast_text" }
]
}
}
| Strategy | Best for |
|---|---|
fast_text | Short values that need exact, substring, fuzzy, or pinyin matching, such as names, tags, folders, or statuses. |
full_text | Longer text that benefits from SQLite FTS5 keyword search, BM25 ranking, and tokenization. |
semantic | Meaningful prose that should match by vector similarity. Requires the embedding model. |
Permissions
permissions decides what the connector's process may do. Gety shows it to the user before every install, in the app and in gety connector install, and the user can view it later. Permissions require Gety 0.7.0 or later, so a manifest that declares them must set min_gety_app_version to 0.7.0 or later.
{
"min_gety_app_version": "0.7.0",
"permissions": {
"net": [
"api.notion.com",
"*.notion.so",
"files.example.com:8443",
{ "config_field": "base_url" }
],
"read": [
"~/.codex/sessions",
{
"path": "~/Library/Application Support/Google/Chrome",
"os": ["macos"]
},
{ "config_field": "export_dir" }
],
"env": ["ACME_LOG_LEVEL"],
"run": ["git"]
}
}
Modes
permissions | Mode |
|---|---|
An object, including {} | Sandbox. The connector gets base access plus exactly what it lists. Everything else is denied. |
Omitted, or "unrestricted" | Full access. The connector runs without a sandbox and can do anything the user can. Gety shows a red warning and asks the user to confirm a second time. |
Entries
| Key | Entry |
|---|---|
net | "*" (any host); host or host:port; *.domain (the domain and its subdomains, no port); an IPv4 address or [IPv6] address, optionally with a port. Hosts are lowercase ASCII; write internationalized names in punycode. Write a host, not a URL. |
read | "~" or "~/dir/…", granting that folder and everything below it. { "path": "~/…", "os": ["macos", "windows", "linux"] } applies only on the listed systems. Absolute paths aren't accepted. |
env | Environment variable names the connector may read. |
run | Program names, without paths, that the connector may start. See Running programs. |
{ "config_field": "<id>" } | Takes the grant from a config field: a text field in net (a URL or host[:port]), a directory field in read. An empty value grants nothing. Password fields can't be referenced. |
Gety checks values taken from config fields when the user installs the connector or saves its configuration: a net value must be a URL or host[:port] without wildcards, and only its host and an explicit port are granted; a read value must be an absolute path. When the user changes the field, the grant follows the new value without another review.
~ means the home folder Gety passes in. Use this.homeDir and this.expandHome(path) rather than reading HOME from the environment, which needs an env grant and may not match the folder Gety grants.
env grants reading a variable, not access to what it points to. If a location can move, such as a tool that honors its own *_HOME variable, let the user pick it in a directory config field and grant it with { "config_field": "<id>" } in read.
Any of these makes a connector high-risk, and the user confirms the install a second time:
"*"or a single-label wildcard such as*.cominnet- Any
runentry "~"inread
Running programs
A program the connector runs isn't limited by the sandbox, so a run entry is equivalent to full access.
Gety resolves each run name through PATH when the connector starts, and only that exact path may be started; the same program at another path, or through its resolved symlink target, is denied. A name not found on PATH grants nothing. On macOS, Gety appends /opt/homebrew/bin and /usr/local/bin to PATH when missing, because an app started from Finder gets only a minimal PATH. Folders that shells add at startup, such as nvm's or Volta's, aren't on that PATH, so programs installed with them aren't found. Ask users to link the program into one of the folders above, or run the connector with full access.
Base access
Every sandboxed connector can, without declaring it:
- Read and write its data folder,
this.dataDir, which is also the working directory and keeps its contents between syncs and across versions. - Read and write a temporary folder (
TMPDIR,TMP,TEMP) that is deleted after each sync. - Read basic system information: the home folder, CPU count, memory, OS release, uptime, load average, and disk usage.
- Read its own code.
Each connector also gets its own localStorage, Cache API, and module cache, not shared with other connectors. The permission review doesn't list base access.
Writing outside these folders, ffi, the hostname, network interfaces, and user or group IDs are never available in the sandbox. A connector that needs them has to run with full access.
Permission errors
When the sandbox denies an access, the sync fails with a stable message, for example:
permission denied: net access to "api.example.com:443" is not granted by the connector's manifest
write, sys, ffi, and import accesses report is not available to sandboxed connectors instead, because no manifest entry grants them. Add the missing entry to permissions, or remove the access from the code.
- Every redirect hop of a
fetchis checked, so declare redirect targets and CDN hosts too. - Node-compatible APIs such as
node:httpreport a denied host asgetaddrinfo EPERM <host>without naming the permission. Preferfetch. - Reading an environment variable that isn't granted doesn't fail:
Deno.env.getreturnsundefined, as if it were unset.
Limitations
- Sandboxed connectors can't import remote (
https:) ornpm:modules at run time. Bundle dependencies into the connector folder. - Files the connector writes can't be opened through document links. See Doc link.
SDK lifecycle
A connector is a default-exported class extending Connector<Config, State>.
import { Connector, type PollResult, del, upsert } from '@gety-ai/connector-sdk';
import type { ManifestConfig } from './gen/manifest.d.ts';
type State = {
cursor?: string;
};
export default class MyConnector extends Connector<ManifestConfig, State> {
async onLoad(): Promise<void> {
// Optional. Runs once in a fresh runtime process before poll().
}
async *poll(): AsyncGenerator<PollResult, void, unknown> {
let cursor = this.lastState?.cursor;
for await (const page of fetchPages(this.config.api_key, cursor, this.signal)) {
yield {
updates: page.items.map((item) => upsert(toDoc(item))),
state: { cursor: page.next_cursor },
};
cursor = page.next_cursor;
}
}
}
| Member | Description |
|---|---|
this.config | Typed configuration injected from Gety's install or configuration form, shaped by config.fields. |
this.lastState | The last state yielded by the last successful poll, or null on the first run. |
this.signal | AbortSignal that aborts when Gety stops the poll, for example when the user disables or restarts the connector. Pass it to fetches. |
this.dataDir | Absolute path of the connector's data folder, which persists between polls. |
this.homeDir | Absolute path of the user's home folder, as Gety resolves ~ in permissions.read. |
this.expandHome(path) | Resolves a leading ~ against this.homeDir. Other paths are returned unchanged. |
onLoad() | Optional, may be async. Runs once in a fresh runtime process before poll(). |
poll() | Required async generator. Yields PollResult batches. |
Lifecycle facts:
- Each scheduled poll starts a fresh connector runtime process.
- Class fields don't persist across polls. Use
state, or files inthis.dataDir, for durable data.
Runtime limits
- Idle timeout. A poll that sends nothing to Gety for 10 minutes is treated as hung and cancelled. Yielded batches and console output both count. During a long operation, yield a batch or log a low-frequency progress line such as
console.info("still syncing..."). Avoid high-frequency logging. - Cancellation. When Gety stops a poll,
this.signalaborts. Check it during long operations, pass it tofetch(), and release temporary files and other resources. Cancellation is cooperative first; if the connector hasn't exited 5 seconds later, Gety kills the process. - Message size. The SDK splits large batches into smaller messages. Each single document update must stay under 16 MiB.
- State size. The next poll starts with the configuration and the state together, within 16 MiB. A yielded state that would exceed it fails the poll.
WireDoc fields
Build documents with upsert(doc) and remove them with del(id).
upsert({
id: issue.id,
title: issue.title,
content: markdown,
content_format: 'markdown',
doc_type: 'linear:issue',
doc_updated_at: issue.updatedAt,
original_file_size: markdown.length,
metadata: {
url: issue.url,
created_at: issue.createdAt,
status: issue.status,
},
});
| Field | Required | Description |
|---|---|---|
id | Yes | Stable document ID, unchanged across polls. |
title | Yes | Document title. Always indexed. |
content | No | Text Gety indexes and previews. |
content_format | No | plaintext (default) or markdown. markdown enables Markdown rendering in the text preview. It controls rendering, not source type or how the document opens. Use markdown only for real Markdown. |
doc_type | No | Usually <namespace>:<type>, such as linear:issue, for external records. Use file:<ext> only when the document is genuinely file-like and should use file-extension presentation. Don't label an external record as a file just to get Markdown preview; set the real record type and use content_format for rendering. |
doc_updated_at | No | RFC 3339 source update time with Z or an offset. Use the source document's update time, not the indexing time. |
parent_id | No | ID of a parent document for hierarchical relationships. |
hide_from_search | No | Hides the document from search while preserving it for parent or relationship use. |
original_file_size | No | Size in bytes, used for display. |
metadata | No | Arbitrary key/value map. metadata.url is the conventional link target. If present, metadata.created_at must be an RFC 3339 string or the poll fails. Extra keys become searchable through schema.extra_gety_indexed_fields. |
Only id and title are required; other unset fields are dropped from the wire. Gety keeps the first 10 MB (UTF-8 bytes) of content and truncates the rest. Emit pages or chunks instead of huge individual documents or metadata blobs.
Poll and state
poll() yields PollResult batches. Each batch carries updates and an optional state checkpoint.
yield {
updates: [
upsert(docA),
upsert(docB),
del(staleId),
],
state: { cursor: page.next_cursor },
};
Gety saves the last state a poll yields only after the whole poll succeeds; it becomes lastState in the next poll. If a poll fails or is cancelled, the next poll starts again from the state of the last successful poll. Gety may already have indexed part of the failed poll's updates, because it applies a batch in bounded units rather than as a single transaction.
Rules:
- Make updates safe to replay. Running a poll again from the same state must end with the same documents. Derive updates from
lastStateand stable document IDs. - Checkpoint after updates. The
stateyou yield with a batch should describe progress reached after that batch's updates. - Don't advance state early. Never yield state for documents you haven't yielded yet.
- Emit deletes from real signals. Call
del(id)only when the source proves the document disappeared or left scope. - Yield at resumable boundaries. For remote APIs, yield per page or durable cursor. For local enumerations, yield per file or chunk.
- Handle timestamp cursors carefully. Keep enough state to avoid missing updates that share the same timestamp.
- Keep state small. Store cursors, not data. Keep larger data in
this.dataDir. - Page or stream the source. The SDK splits large batches into smaller messages, but your connector should still page or stream source data instead of holding everything in memory.
Validation
Run the sample verification task before installing:
deno task verify
verify runs fix, check, test, and build:
fixruns formatting and lint fixes.checkgenerates the config type and type-checks the project.testruns tests.buildwrites the bundled entry loaded by Gety.
You can also run the steps individually:
deno task fix
deno task check
deno task test
deno task build
Exercise the real lifecycle with the local runner:
cp .env.example .env
deno task runner -- --reset-state
deno task runner -- --polls 2
The runner runs each poll the way Gety does: in a fresh Deno process, in Gety's sandbox with the manifest's permissions, or with full access when the manifest declares none. A denied access fails the poll with the same message Gety shows. --permissions '<json>' tries other permissions without editing the manifest, and --full-access runs without the sandbox. this.dataDir is dev/.runner/data/ and is kept between runs.
Like Gety, the runner saves state only when a poll succeeds, at dev/.runner/state.json. --reset-state ignores it for a clean first poll. Each run writes a snapshot under dev/runs/<timestamp>/:
dev/runs/<timestamp>/
summary.json
state.before.json
state.after.json
updates.json
deletes.json
docs/
0001-<doc_type>__<doc_id>.md
0001-<doc_type>__<doc_id>.json
The content file is .md for content_format: "markdown" and .txt otherwise. It is omitted when the document has no content. The .json sidecar is always written. Inspect the output for stable IDs, expected content, correct metadata.url, RFC 3339 timestamps, deletes, and before/after state behavior.
After editing src/index.ts, run deno task build, then click More actions (⋯) → Restart in Gety to load the new dist/main.js.
Publishing
Users install published connectors from a GitHub release or an npm package. Gety never builds from source and never calls the GitHub API. It installs only the published files, records the SHA-256 of what it downloaded, and keeps its own copy of the code. For a walkthrough, see Publish your connector.
GitHub releases
Publish one release per version. The tag is the manifest version, optionally prefixed with v (1.2.0 or v1.2.0). Each release carries two assets:
manifest.json, byte-for-byte identical to themanifest.jsonat the root of the code package. Before the user confirms, Gety downloads only this file and shows the permissions it declares.<id>-<version>.zip, withidandversiontaken from the manifest. Its root is the connector folder:manifest.json, the entry, and every other file the connector needs at run time, such as its icon, source map, and data files, without a wrapping folder.
Code package limits: at most 100 MiB, at most 20,000 files and 500 MiB unpacked, no symbolic links, and every file stored or compressed with deflate.
When the user confirms, Gety downloads the code package and manifest.json again, and refuses to install unless the package's manifest.json is byte-for-byte identical to that asset and its id and version match the ones the user reviewed. Turn on immutable releases in the repository settings so a published tag and its assets can't change.
By default, Gety installs the release GitHub marks as Latest. Pre-releases install only through their release link (https://github.com/<owner>/<repo>/releases/tag/<tag>) or --version.
npm packages
The package's package.json carries the whole manifest in a gety object:
{
"name": "@acme/gety-linear",
"version": "1.2.0",
"homepage": "https://github.com/acme/gety-linear#readme",
"files": ["dist/main.js", "icon.svg", "CHANGELOG.md"],
"gety": {
"id": "acme-linear",
"name": "Acme Linear",
"version": "1.2.0",
"min_gety_app_version": "0.7.0",
"entry": "dist/main.js",
"icon": "icon.svg",
"capabilities": ["data-source"],
"permissions": { "net": ["api.linear.app"] }
}
}
getyis the complete manifest, andgety.versionmust equal the packageversion, compared as SemVer including pre-release and build metadata.filesmust include the entry and every other file the connector needs at run time. ListCHANGELOG.mdtoo: npm doesn't pack it on its own oncefilesis set.- Prefix
idwith your own name (acme-linear, notlinear): a connector with the same ID can't be installed next to yours. - A
manifest.jsonin the package isn't read; onlygetycounts. When you publish to both GitHub and npm, keepmanifest.jsonas the single source and generate thegetyobject from it. - Set
homepageto a URL that explains the connector. - The package must be public. Gety installs the version the
latestdist-tag points to, or the exact version the user names (@acme/gety-linear@1.3.0-beta.1,https://www.npmjs.com/package/@acme/gety-linear/v/1.3.0-beta.1, or--version). Other dist-tags and version ranges aren't supported.
The package tarball follows the same limits as a GitHub code package: at most 100 MiB, at most 20,000 entries and 500 MiB unpacked, and only regular files and folders under package/, with no symbolic or hard links. The registry must list a sha512 integrity for it.
Before the user confirms, Gety shows the permissions declared in the gety object of the registry's metadata for that version. When the user confirms, Gety downloads the tarball, checks it against the registry's integrity, and refuses to install unless the gety object in the tarball's package.json equals the one the user reviewed. npm never lets a published version change, so changing permissions means publishing a new version.
When the system region is mainland China, Gety reads the package from npmmirror first and falls back to registry.npmjs.org. The mirror usually syncs a new version within a few minutes.
Versions and updates
Gety checks the registry for newer versions of connectors installed from npm and lets users switch to any published version, newer or older, in the app or with gety connector update. A version is offered as an update when it is higher than the installed one, isn't a pre-release, isn't deprecated, and runs on the user's Gety. The latest dist-tag plays no part in this. Connectors installed from GitHub or a local folder have no version management.
Raising min_gety_app_version doesn't strand users on older Gety versions: they see that the version needs a newer Gety, and can install an older one.
Release notes. Gety shows the CHANGELOG.md at the root of the package, split into sections by version headings. ## 1.2.0, ## v1.2.0, and ## [1.2.0] - 2026-07-14 all work, and headings inside code blocks are ignored. Use the same heading level for every version. A section ends at the next heading of the same or a higher level; a heading there that isn't a version, such as ## Unreleased, isn't shown, nor is its content. A --- line after a blank line counts as such a heading, so text after it is cut. Keep the sections of older versions in the file. Changesets writes this format. Only the first 1 MiB is shown.
sync_version. A whole number, 1 when absent. It's the format version of what the connector has synced: its state, its document IDs, and the content of its documents. Raise it in a release that can't continue from what an older release synced, such as when the state changes shape or document IDs change. Gety then deletes the connector's documents, clears its sync state, and syncs from scratch on upgrade. Changing extra_gety_indexed_fields does the same without raising sync_version.
Downgrades always delete the connector's documents and sync from scratch: an older release can't be expected to read the state a newer one wrote.
Withdrawing a version. Published versions can't change. To withdraw a broken one, deprecate it:
npm deprecate @acme/gety-linear@1.2.0 "Loses comments; use 1.2.1"
Gety never offers a deprecated version as an update. Users still see it, marked as deprecated with your message, and can choose it explicitly. Pointing latest back at an older version doesn't withdraw a newer one.
What carries over. The connector's data folder (this.dataDir) and its localStorage are kept across versions; the sync state is cleared only when the switch resyncs. The user's settings carry over field by field: a field that keeps its id and its type keeps the user's value, secrets included. A field whose type changes is reset to the new default, a field that's gone is removed, and a new field starts at its default. When a required field ends up empty, or a value isn't among the new options, the user fills it in before switching. A switch also asks the user to review any permissions the new version adds.
Featured list
Connectors published on npm can be listed in the Community tab of Featured connectors in Gety by registering them in gety-ai/gety-connectors. See Publish to the featured list and the contributing guide.