@zapier/connectors-dev
v0.18.1
Published
Standard dev tooling and validator for connectors: validate (with --fix), stage, and scaffold-connector.
Readme
@zapier/connectors-dev
Standard dev tooling and validator for connectors built with @zapier/connectors-sdk. Provides a unified connectors-dev CLI and a programmatic API:
| Command | What it does |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| validate | Validate connectors against agentskills.io rules (via skills-ref) plus the connector contract (package.json, index.ts, cli.ts, scripts/*.ts, evals/evals.json). With --fix, apply every fixable rule instead of reporting: shared assets, index.ts wiring, and package.json fundamentals. Vendor-specific policy (e.g. NOTICE / legal fragments / LICENSE) lives in an extends config package such as @zapier/connectors-dev-config. |
| scaffold-connector | Bootstrap a new connector shell: package.json, connections.ts, SKILL.md, index.ts, and placeholder directories. |
| stage | Copy a connector into a clean tmp directory (git-tracked files only) for standalone install testing — mirrors what an agentskills.io client receives. |
| doc-regions | Print the canonical instructions for a BEGIN:/END: doc-region slug (or every slug, with no arguments) as JSON — the source of truth for what belongs in a region, even after its BEGIN comment's instructions have been stripped from a live file. |
| register | Register a connector's tools on Zapier so they can be discovered and run from any Zapier surface. Reconciles against what's already registered — creates new tools, updates existing ones, and deprecates ones no longer in the build; --dry-run prints the payloads instead. |
| publish | Make a connector's already-registered tools public. Publishes every tool that isn't public yet and whose lifecycle is published (or unset), skipping already-public and deprecated ones; --dry-run lists them instead. Publishing is one-way — once a tool is public, it can't be deleted, only deprecated. |
CLI
# Validate a single connector
connectors-dev validate ./apps/my-connector
# Validate every connector under apps/
# (a path that isn't itself a connector is treated as a folder of connectors)
connectors-dev validate apps
# Apply every fixable rule (shared assets, index.ts, package.json, + any config rules)
connectors-dev validate apps/my-connector --fix
# Fix every connector under apps/
connectors-dev validate apps --fix
# Bootstrap a new connector
connectors-dev scaffold-connector ./apps/my-connector --slug my-connector
connectors-dev scaffold-connector ./apps/my-connector --slug my-connector --license Elastic-2.0
# Stage a connector for standalone testing
connectors-dev stage apps/notion
# → prints path to tmp dir; capture it: path=$(connectors-dev stage apps/notion)
# Stage and also produce a ZIP
connectors-dev stage apps/notion --zip
# Vendor the local SDK source into the staging dir (test SDK changes before publishing)
connectors-dev stage apps/notion --local-sdk packages/connectors-sdk
# Print every region's canonical instructions, or just one slug's
connectors-dev doc-regions
connectors-dev doc-regions skill-intro
# Build the registration payloads and print them, without sending anything
connectors-dev register apps/notion --dry-run
# Reconcile the connector's tools against the registry: create the new ones,
# update the ones that already exist, deprecate the ones no longer in the build
connectors-dev register apps/notion
# Keep going past a tool that fails to build or register, reporting every failure at the end
connectors-dev register apps/notion --force
# List the connector's already-registered tools that are eligible to publish, without publishing them
connectors-dev publish apps/notion --dry-run
# Publish them: makes every eligible tool public (irreversible — see Publishing below)
connectors-dev publish apps/notion
# Keep going past a tool that fails to publish, reporting every failure at the end
connectors-dev publish apps/notion --forceRun connectors-dev with no arguments for the full option reference.
Programmatic API
import {
scaffoldConnector,
validate,
formatIssues,
evalsJsonSchema,
describeEvalsIssue,
} from "@zapier/connectors-dev";
// Validate connectors and format any issues. The rule set and package-name
// pattern come from connectors-dev.config.ts (loaded from the cwd).
const results = await validate(["./apps/my-connector"]);
for (const [connectorPath, issues] of results) {
console.error(formatIssues(connectorPath, issues));
}
// Scaffold a new connector. The rule set and package-name pattern come from
// connectors-dev.config.ts too (loaded from `cwd`, defaulting to process.cwd()).
const { connectorDir, filesWritten } = await scaffoldConnector({
slug: "my-connector",
license: "Elastic-2.0",
out: "./apps/my-connector",
});
// Parse/validate evals.json against the same schema the validator uses
const parsed = evalsJsonSchema.safeParse(rawEvals);
if (!parsed.success) {
for (const issue of parsed.error.issues)
console.error(describeEvalsIssue(issue));
}Validation
connectors-dev validate <path>... composes two layers:
- agentskills.io
skills-ref—SKILL.mdfrontmatter and skill-folder rules. - Connector contract —
package.json, bundleindex.ts/cli.ts,scripts/*.ts(defineTool+handleIfScriptMain),evals/evals.json, andSKILL.mdextensions (metadata.api-docs,compatibility,metadata.source).
Vendor-specific policy (e.g. a scoped package name, LICENSE / NOTICE, legal fragments) is not built in — it arrives through an extends config package such as @zapier/connectors-dev-config. A connector copied out of the monorepo runs with no config and is therefore only held to the brand-neutral contract above.
Each path is auto-detected: a directory with a package.json is a single
connector, otherwise its immediate subdirectories with a package.json are
validated (a folder of connectors).
Check and --fix report identically — each issue is one <icon> path [rule] message
line, where the icon is its status: ✅ fixed, 🤖 fixable (rerun with --fix),
⛔️ non-fixable (fix by hand). A trailing summary tallies Fixed / Fixable /
Non-fixable. Exit code is 0 only when nothing blocks: a check run passes when
there are no issues, a --fix run passes when nothing non-fixable remains
(✅-fixed issues don't fail it). Blocking runs print to stderr and exit 1.
Options
| Flag | Purpose |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| --fix | Apply every fixable rule instead of reporting (shared assets, index.ts, package.json, plus any config-supplied rules) |
| --ignore-rule <rule> | Ignore a rule for this run only (repeatable: --ignore-rule foo --ignore-rule bar). Unioned with the config's ignoreRules — never modifies the config file itself. |
The rule set and package.json name policy come from
connectors-dev.config.ts (or the built-in defaults) — there
is no per-run name-pattern flag.
What it checks
| Layer | Rule tag | Method |
| ---------------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| SKILL.md | agentskills | Upstream skills-ref validate() |
| SKILL.md metadata.api-docs | api-docs | HTTPS URL presence — agents need it to look up endpoints and auth schemes |
| SKILL.md compatibility | compatibility | Required field with pinned value — harnesses read it to decide if they can run the connector |
| SKILL.md metadata.source | source | Present + valid https:// URL (the exact canonical URL is a vendor policy, enforced by an extends config) |
| scripts/, package.json, evals/evals.json | scripts-dir, package.json, evals.json | JSON + filesystem (scripts-dir fixably creates a missing scripts/) |
| index.ts, scripts/*.ts | index.ts, scripts | TypeScript compiler API (AST) |
| scripts/*.ts runtime shape | scripts | Dynamic import + getToolDefinitionShapeIssues |
| cli.js, cli.ts, tsup.config.ts | cli.js, cli.ts, tsup.config.ts | Byte-identical sync to the canonical assets/ copy |
Rules & fixing
validate and validate --fix are the same pass over the same rule set — the
only difference is the fix flag on the context. A ConnectorRule is a single
function that inspects one connector, reports findings via ctx.report, and
repairs what it can when ctx.fix is set:
interface RuleContext {
connectorDir: string;
packageNamePattern: string;
fix: boolean;
report(finding: {
message: string;
file?: string;
line?: number;
fixStatus?: "fixable" | "fixed";
}): void;
}
type ConnectorRule = (ctx: RuleContext) => void | Promise<void>;
interface RuleEntry {
name: string;
rule: ConnectorRule;
}A rule never names itself: the name lives on the registry RuleEntry, and the
runner stamps it onto every reported finding. Findings carry an optional
fixStatus — "fixable" in a check run, "fixed" after a fix run — so the CLI
can report how many issues a --fix repaired and which still need hand-editing.
runRule(entry, base) runs a single rule against one connector and returns the
stamped findings; the base context's fix flag is the only thing that makes it
a check run versus a fix run. It's the unit-test entry point for a rule. Rules get
no special treatment — a builtin rule is the same shape a downstream
config/policy package would provide, so a rule can move out of this package
unchanged.
Registration
connectors-dev register <dir> reads the connector's package.json for its
package name and version, imports its index.ts, and turns every tool the
connector registers into one registration payload carrying that tool's name,
description, annotations, input/output schemas as JSON Schema, and version
(major/minor/patch, parsed from the package's semver).
The Zapier app the tools belong to is sent as the connector's slug, taken from
the package name. The registry resolves it to that app's canonical
selected_api and stores the resolved value, so google-sheets lands as
GoogleSheetsV2CLIAPI. A slug that matches no app is rejected with a 400.
Each tool is identified by its package name and tool name joined with #, the
same shape the runtime parses when it executes the tool. Both halves are checked
against the patterns the runtime enforces before anything is sent, so a tool that
could never be executed is never registered.
Sending (not --dry-run) reconciles against what's already registered instead
of always creating: it looks up the connector's existing tools — filtered by
the zapier-app-key declared in its SKILL.md, since the registry's list
filter doesn't resolve slugs the way the create path does — and per tool,
creates the ones that are new, updates the ones that already exist, and
deprecates the ones no longer in the build.
One unregistrable or rejected tool stops the whole run. --force keeps going
past a failure — build or registration — and reports every one it hit at the
end. Either way the command exits 1 if anything failed.
| Flag | Purpose |
| ----------- | ------------------------------------------------------------------------------------------- |
| --dry-run | Print the payloads as JSON instead of reconciling them against the registry. |
| --force | Keep going past a tool that fails to build or register, reporting every failure at the end. |
| Variable | Required |
| ---------------------------- | ------------------------------------------------------------------------------------------- |
| ZAPIER_REGISTRY_CUSTOMUSER | Always, dry runs included — it's part of the payload. |
| ZAPIER_REGISTRY_TOKEN | To send. Checked when the send starts, so a dry run needs no token. |
| ZAPIER_REGISTRY_URL | No — defaults to https://zapier.com. Set it to target another environment (e.g. staging). |
ZAPIER_REGISTRY_CUSTOMUSER is read before the connector is, so a run missing
it fails before anything is loaded or built.
Publishing
connectors-dev publish <dir> makes a connector's already-registered tools
public. It looks up the connector's existing tools — the same lookup
register uses, filtered by the zapier-app-key declared in its SKILL.md
— and publishes the ones that aren't public yet and whose lifecycle is
published (or unset), skipping tools that are already public, deprecated,
deprecating, or legacy.
Unlike register, publish doesn't decide on its own which tools should go
public — it's a manual command, and whoever runs it decides. It doesn't read
any pipeline or connector-level public/private signal.
Publishing a tool is one-way. Once a tool is public, the registry refuses
to delete it (409 Conflict); the only way to take it out of circulation
afterward is to deprecate it, which doesn't undo the public status. Because
of that, tools published before a failure stay public even without
--force — the summary always prints so that partial progress isn't lost.
| Flag | Purpose |
| ----------- | --------------------------------------------------------------------------------- |
| --dry-run | List the eligible tools instead of publishing them. |
| --force | Keep going past a tool that fails to publish, reporting every failure at the end. |
| Variable | Required |
| ----------------------- | ------------------------------------------------------------------------------------------- |
| ZAPIER_REGISTRY_TOKEN | Yes, dry runs included — listing the eligible tools already reads the registry. |
| ZAPIER_REGISTRY_URL | No — defaults to https://zapier.com. Set it to target another environment (e.g. staging). |
Configuration
validate, validate --fix, and scaffold-connector read an optional
connectors-dev.config.{ts,js,mjs} from the working directory (loaded with
c12, so extends layers and a sibling .env
work). A connector copied out of the monorepo has no config and falls back to
the built-in defaults, which is correct for a standalone install.
import { defineConnectorsDevConfig } from "@zapier/connectors-dev";
export default defineConnectorsDevConfig({
// Pull in a shared policy package (resolved + merged first).
extends: "@zapier/connectors-dev-config",
// package.json name pattern; {name} = connector slug. Default "{name}-connector".
packageNamePattern: "@zapier/{name}-connector",
// Drop builtin rules by name…
ignoreRules: ["legal"],
// …and append your own (same RuleEntry shape as a builtin).
rules: [{ name: "house-style", rule: (ctx) => {} }],
});| Field | Default | Purpose |
| -------------------- | -------------------- | ----------------------------------------------------------------------------------- |
| packageNamePattern | "{name}-connector" | package.json name pattern; {name} is the connector slug (directory basename). |
| extends | (none) | Local path(s) or package(s) to inherit and deep-merge (a later layer wins). |
| ignoreRules | [] | Builtin rule names to drop from the run. |
| rules | [] | Extra RuleEntry objects appended after the builtins. |
A config-supplied rule runs through the exact same path as a builtin — there is
no builtin-vs-external branch — so a rule can migrate to a downstream policy
package (dropped here via ignoreRules, added there via rules) with no engine
change. packageNamePattern is config-only: there is no --naming flag, so
validate and scaffold-connector always agree on the package name a connector
should carry.
Assets
The package ships canonical connector asset files under assets/:
| File | Purpose |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| cli.js | Plain-JS connector CLI proxy — runs dist/cli.js when present (npm install route) or falls back to cli.ts (Node 22.18+). Ports the old preflight readiness checks: bails with actionable guidance when node_modules is missing or the .ts source can't run on this Node. |
| cli.ts | TypeScript connector CLI entry point (no shebang; executed via cli.js). |
| tsup.config.ts | tsup build config shared by all connectors. |
Each asset is enforced by its own byte-identical rule (createAssetRule): content equality is the whole contract, so no separate shebang / AST / exec-bit check is needed. These files are the source of truth — edit here; run connectors-dev validate <dir> --fix (or the monorepo's pnpm run validate:fix) to propagate changes to every connector.
scaffold-connector options
Invoked as scaffold-connector <dir> --slug <slug> [options] — the output directory is the one positional, the slug is a required named option.
| Option | Default | Notes |
| ---------------------- | ----------------- | ------------------------------------------------------------------ |
| --slug <slug> | (required) | Connector slug (lowercase, digits, hyphens; starts with a letter) |
| --title <name> | titleCase(slug) | Human-readable app display name |
| --license <spdx> | (none) | SPDX license identifier written into package.json and SKILL.md |
| --description <text> | auto | One-sentence description for package.json and SKILL.md |
| --api-docs <url> | (placeholder) | Vendor API docs URL for SKILL.md frontmatter |
| --force | false | Overwrite an existing directory |
The package name follows the packageNamePattern from
connectors-dev.config.ts (default {name}-connector), so a scaffolded
connector passes validate without a separate scope flag.
