#!/usr/bin/env python3 """Document every Zapier CLI / core / SDK function in MCP-reference format. Writes kind=cli_function, core_function, sdk_function (and upgrades the legacy cli_command / sdk_command rows) plus FUNCTIONS-REFERENCE.md and a guide:zapier-functions playbook. """ from __future__ import annotations import json import os import re import sys from datetime import datetime, timezone from pathlib import Path ROOT = Path(os.environ.get("ZAPIER_RESEARCH_ROOT") or Path(__file__).resolve().parents[1]) REPOS = ROOT / "repos" NOW = datetime.now(timezone.utc).isoformat() PLATFORM_CLI_MD = ( REPOS / "zapier-platform" / "packages" / "cli" / "docs" / "cli.md" ) if not PLATFORM_CLI_MD.exists(): PLATFORM_CLI_MD = Path("/Users/marchon/research/zapier-platform/packages/cli/docs/cli.md") SDK_CLI_MD = REPOS / "sdk" / "skills" / "zapier-sdk" / "references" / "cli-commands.md" def kebab_to_camel(name: str) -> str: parts = name.replace(":", "-").split("-") if not parts: return name return parts[0] + "".join(p[:1].upper() + p[1:] if p else "" for p in parts[1:]) # --------------------------------------------------------------------------- # Core runtime (z.*) — hand-authored, MCP-quality # --------------------------------------------------------------------------- CORE = [ { "key": "z.request", "category": "http", "title": "Authenticated HTTP to the partner API", "signature": "z.request(url, options?) → Promise\nz.request(optionsWithUrl) → Promise", "high_level": "The only HTTP client you should use inside a perform. Adds auth middleware, logging, and status checks.", "internals": ( "Runs the beforeRequest chain (basic/digest/oauth1 headers, query merge, prepare-request), " "then Node fetch, then afterResponse (throw-for-status, stale-auth → RefreshAuthError, " "throttling, log-response). `json` serializes the body; `form` sends urlencoded; `params` " "are query string; `raw:true` returns a stream (RawHttpResponse). Non-2xx throws " "ResponseError unless skipThrowForStatus. Same path Zapier uses in production." ), "inputs": """```ts type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "HEAD"; type Input = | [url: string, options?: HttpRequestOptions] | [options: HttpRequestOptions & { url: string }]; interface HttpRequestOptions { method?: HttpMethod; headers?: Record; body?: string | Buffer | NodeJS.ReadableStream | Record; json?: unknown; // JSON body form?: unknown; // application/x-www-form-urlencoded params?: Record; raw?: boolean; skipThrowForStatus?: boolean; removeMissingValuesFrom?: { params?: boolean; body?: boolean }; skipEncodingChars?: string; middlewareData?: Record; timeout?: number; redirect?: "manual" | "error" | "follow"; } ```""", "outputs": """```ts interface HttpResponse { status: number; headers: Headers; content: string; data: T; // parsed JSON when content-type is JSON throwForStatus(): void; getHeader(key: string): string | undefined; request: HttpRequestOptions; } // raw:true → RawHttpResponse with .body stream, .buffer(), .json(), .text() ```""", "related": ["BeforeRequestMiddleware", "AfterResponseMiddleware", "z.errors.RefreshAuthError", "z.errors.ResponseError", "z.console"], "examples": [ {"title": "GET with auth middleware", "code": "const resp = await z.request('https://api.example.com/v1/contacts');\nreturn resp.data;"}, {"title": "POST JSON", "code": "const resp = await z.request({\n url: 'https://api.example.com/v1/contacts',\n method: 'POST',\n json: { name: bundle.inputData.name },\n});\nreturn resp.data;"}, ], "mcp_twin": "execute_zapier_read_action / execute_zapier_write_action (hosted)", "sdk_twin": "zapier.fetch(url, { connection, method }) / zapier-sdk curl", }, { "key": "z.console", "category": "debug", "title": "Integration logger", "signature": "z.console.log|info|warn|error(...args) → void", "high_level": "Logs that show up in zapier-platform logs and Zap history. Prefer this over console.log.", "internals": "Forwards to Zapier's log pipeline (type=console). Local `zapier-platform test` prints to stdout; production is queried via `zapier-platform logs --type console`.", "inputs": "```ts\n(...args: unknown[]) => void\n```", "outputs": "```ts\nvoid\n```", "related": ["zapier-platform logs"], "examples": [{"title": "Debug a payload", "code": "z.console.log('input', bundle.inputData);"}], "mcp_twin": None, "sdk_twin": None, }, { "key": "z.dehydrate", "category": "hydration", "title": "Lazy pointer to an expensive object", "signature": "z.dehydrate(func, inputData?, cacheExpiration?) → string", "high_level": "Returns a pointer string. Zapier later calls func(z, bundle) when a downstream step needs the data.", "internals": "Serializes {method path, inputData} into a hydrate token. At hydrate time the runtime resolves the function from App.hydrators / the original module and invokes it. Use for large payloads you do not want in every trigger item.", "inputs": "```ts\n(func: (z: ZObject, bundle: Bundle) => any, inputData?: T, cacheExpiration?: number) => string\n```", "outputs": "```ts\nstring // hydrate pointer, not the payload\n```", "related": ["z.dehydrateFile", "z.stashFile", "HydratorsSchema"], "examples": [{"title": "Defer a contact fetch", "code": "return {\n id: contact.id,\n name: contact.name,\n extra: z.dehydrate(getFullContact, { id: contact.id }),\n};"}], "mcp_twin": None, "sdk_twin": None, }, { "key": "z.dehydrateFile", "category": "hydration", "title": "Lazy pointer to a file", "signature": "z.dehydrateFile(func, inputData?, cacheExpiration?) → string", "high_level": "File-specific dehydrator. Zapier fetches bytes only when a later step needs the file.", "internals": "Same pointer mechanism as z.dehydrate but marked as a file so the editor treats it as a file field. See example-apps/files.", "inputs": "```ts\n(func: (z: ZObject, bundle: Bundle) => any, inputData?: T, cacheExpiration?: number) => string\n```", "outputs": "```ts\nstring\n```", "related": ["z.dehydrate", "z.stashFile"], "examples": [{"title": "Trigger item with a file", "code": "return { id: file.id, file: z.dehydrateFile(downloadFile, { id: file.id }) };"}], "mcp_twin": None, "sdk_twin": None, }, { "key": "z.stashFile", "category": "files", "title": "Upload bytes and get a public Zapier URL", "signature": "z.stashFile(input, knownLength?, filename?, contentType?) → string", "high_level": "Turns a Buffer, stream, URL string, or Promise into a short-lived public URL.", "internals": "Uploads to Zapier's file stash. Used by hydrators and file creates. Length/filename/contentType help the stash; streams should pass knownLength when possible.", "inputs": "```ts\n(input: string | Buffer | NodeJS.ReadableStream | Promise | Promise,\n knownLength?: number, filename?: string, contentType?: string) => string\n```", "outputs": "```ts\nstring // https://zapier-dev-files.s3… style URL\n```", "related": ["z.dehydrateFile", "z.request"], "examples": [{"title": "Stash a download", "code": "const raw = await z.request({ url: fileUrl, raw: true });\nreturn { file: z.stashFile(raw, undefined, 'report.pdf', 'application/pdf') };"}], "mcp_twin": None, "sdk_twin": None, }, { "key": "z.cursor.get", "category": "polling", "title": "Read the polling cursor", "signature": "z.cursor.get() → Promise", "high_level": "Per-subscription opaque cursor so a poll can resume from last-seen id/timestamp.", "internals": "Zapier stores one cursor string per user/zap/trigger. Empty string if never set. Pair with z.cursor.set at the end of perform.", "inputs": "```ts\n() => Promise\n```", "outputs": "```ts\nPromise\n```", "related": ["z.cursor.set", "PollingTriggerPerform"], "examples": [{"title": "Incremental poll", "code": "const since = await z.cursor.get();\nconst rows = await fetchSince(since);\nawait z.cursor.set(rows[0]?.updated_at || since);\nreturn rows;"}], "mcp_twin": None, "sdk_twin": None, }, { "key": "z.cursor.set", "category": "polling", "title": "Write the polling cursor", "signature": "z.cursor.set(cursor) → Promise", "high_level": "Persist an opaque cursor for the next poll.", "internals": "Replaces the stored cursor. Keep it small (id or ISO timestamp).", "inputs": "```ts\n(cursor: string) => Promise\n```", "outputs": "```ts\nPromise\n```", "related": ["z.cursor.get"], "examples": [{"title": "Save newest id", "code": "await z.cursor.set(String(newest.id));"}], "mcp_twin": None, "sdk_twin": None, }, { "key": "z.generateCallbackUrl", "category": "callback", "title": "Resume URL for long-running creates", "signature": "z.generateCallbackUrl() → string", "high_level": "Partner API can POST here to resume a create via performResume.", "internals": "Mints a one-shot Zapier URL. Original perform returns quickly; later performResume gets bundle.cleanedRequest + bundle.outputData. See example-apps/callback.", "inputs": "```ts\n() => string\n```", "outputs": "```ts\nstring // https://hooks.zapier.com/… callback\n```", "related": ["CreatePerformResume"], "examples": [{"title": "Kick off async job", "code": "const hook = z.generateCallbackUrl();\nawait z.request({ url: '.../jobs', method: 'POST', json: { callback: hook } });\nreturn { pending: true };"}], "mcp_twin": None, "sdk_twin": None, }, { "key": "z.hash", "category": "crypto", "title": "Hash helper (Node crypto)", "signature": "z.hash(algorithm, data, encoding?, input_encoding?) → string", "high_level": "Convenience around crypto.createHash. Default hex / binary.", "internals": "algorithm typically 'sha256'. Used for cache keys and HMAC-style signatures you compute yourself (OAuth1 signing is automatic).", "inputs": "```ts\n(algorithm: string, data: string, encoding?: string, input_encoding?: string) => string\n```", "outputs": "```ts\nstring\n```", "related": [], "examples": [{"title": "SHA-256 hex", "code": "const digest = z.hash('sha256', bundle.inputData.raw);"}], "mcp_twin": None, "sdk_twin": None, }, { "key": "z.JSON.parse", "category": "json", "title": "JSON.parse with a clean Zapier error", "signature": "z.JSON.parse(text) → any", "high_level": "Prefer over JSON.parse so users see a friendly error on bad JSON.", "internals": "Wraps JSON.parse and rethrows as a user-visible AppError.", "inputs": "```ts\n(text: string) => any\n```", "outputs": "```ts\nany\n```", "related": ["z.JSON.stringify", "z.errors.Error"], "examples": [{"title": "Parse partner body", "code": "const data = z.JSON.parse(resp.content);"}], "mcp_twin": None, "sdk_twin": None, }, { "key": "z.JSON.stringify", "category": "json", "title": "JSON.stringify on z", "signature": "z.JSON.stringify(value) → string", "high_level": "Same as built-in JSON.stringify; kept for symmetry with z.JSON.parse.", "internals": "Delegates to JSON.stringify.", "inputs": "```ts\n(value: unknown) => string\n```", "outputs": "```ts\nstring\n```", "related": ["z.JSON.parse"], "examples": [{"title": "Serialize", "code": "const body = z.JSON.stringify({ ok: true });"}], "mcp_twin": None, "sdk_twin": None, }, { "key": "z.cache.get", "category": "cache", "title": "Read per-auth cache", "signature": "z.cache.get(key) → Promise", "high_level": "JSON-encodable values scoped to the connected account.", "internals": "Zapier-hosted key/value. Use for account-id lookups you would otherwise repeat every poll.", "inputs": "```ts\n(key: string) => Promise\n```", "outputs": "```ts\nPromise // undefined/null if missing\n```", "related": ["z.cache.set", "z.cache.delete"], "examples": [{"title": "Memoize org id", "code": "let org = await z.cache.get('orgId');\nif (!org) { org = (await fetchOrg()).id; await z.cache.set('orgId', org, 3600); }"}], "mcp_twin": None, "sdk_twin": None, }, { "key": "z.cache.set", "category": "cache", "title": "Write per-auth cache", "signature": "z.cache.set(key, value, ttl?, scope?, nx?) → Promise", "high_level": "Store JSON-encodable data. ttl in seconds. nx=true sets only if missing.", "internals": "scope further namespaces the key. Returns whether the write landed.", "inputs": "```ts\n(key: string, value: any, ttl?: number, scope?: string[], nx?: boolean) => Promise\n```", "outputs": "```ts\nPromise\n```", "related": ["z.cache.get", "z.cache.delete"], "examples": [{"title": "TTL 1h", "code": "await z.cache.set('orgId', org.id, 3600);"}], "mcp_twin": None, "sdk_twin": None, }, { "key": "z.cache.delete", "category": "cache", "title": "Delete a cache key", "signature": "z.cache.delete(key) → Promise", "high_level": "Drop stale metadata (e.g. after auth refresh).", "internals": "Removes one key from the per-auth cache.", "inputs": "```ts\n(key: string) => Promise\n```", "outputs": "```ts\nPromise\n```", "related": ["z.cache.get", "z.cache.set"], "examples": [{"title": "Invalidate", "code": "await z.cache.delete('orgId');"}], "mcp_twin": None, "sdk_twin": None, }, { "key": "z.errors.Error", "category": "errors", "title": "User-visible app error", "signature": "throw new z.errors.Error(message, code?, status?)", "high_level": "Primary error to throw from perform. Message is shown to the user.", "internals": "Serializes {message, code, status} as AppError. code/status help Zapier classify retries vs user-fixable failures.", "inputs": "```ts\n(message: string, code?: string, status?: number)\n```", "outputs": "```ts\nnever // throws\n```", "related": ["z.errors.HaltedError", "z.errors.RefreshAuthError", "z.errors.ThrottledError"], "examples": [{"title": "Validation", "code": "throw new z.errors.Error('Name is required', 'InvalidInput', 400);"}], "mcp_twin": None, "sdk_twin": None, }, { "key": "z.errors.HaltedError", "category": "errors", "title": "Stop this run without failing the Zap", "signature": "throw new z.errors.HaltedError(message?)", "high_level": "Filter-out. Use when the payload should be ignored.", "internals": "Marks the run halted, not errored. Zaps stay on. Typical for 'this webhook is not the event we care about'.", "inputs": "```ts\n(message?: string)\n```", "outputs": "```ts\nnever\n```", "related": ["z.errors.Error"], "examples": [{"title": "Ignore other event types", "code": "if (bundle.cleanedRequest.type !== 'invoice.paid') {\n throw new z.errors.HaltedError('not an invoice.paid event');\n}"}], "mcp_twin": None, "sdk_twin": None, }, { "key": "z.errors.ExpiredAuthError", "category": "errors", "title": "Dead connection — user must reconnect", "signature": "throw new z.errors.ExpiredAuthError(message?)", "high_level": "Revoked tokens that cannot be refreshed.", "internals": "Pauses the Zap and emails the user to reconnect. Do not use for a refreshable 401 — that is RefreshAuthError.", "inputs": "```ts\n(message?: string)\n```", "outputs": "```ts\nnever\n```", "related": ["z.errors.RefreshAuthError", "manage_zapier_connections"], "examples": [{"title": "Revoked", "code": "if (resp.status === 403 && resp.data.error === 'revoked') {\n throw new z.errors.ExpiredAuthError('Reconnect this account');\n}"}], "mcp_twin": "manage_zapier_connections", "sdk_twin": "create-connection / wait-for-new-connection", }, { "key": "z.errors.RefreshAuthError", "category": "errors", "title": "Refresh OAuth/session and retry", "signature": "throw new z.errors.RefreshAuthError(message?)", "high_level": "Typical 401 handler. Zapier calls refreshAccessToken / sessionConfig.perform then retries.", "internals": "Thrown automatically by afterResponse when it sees stale auth; you can also throw it yourself. Only works for oauth2/session with refresh configured.", "inputs": "```ts\n(message?: string)\n```", "outputs": "```ts\nnever\n```", "related": ["z.errors.ExpiredAuthError", "AfterResponseMiddleware", "AuthenticationOAuth2ConfigSchema"], "examples": [{"title": "401", "code": "if (resp.status === 401) throw new z.errors.RefreshAuthError();"}], "mcp_twin": None, "sdk_twin": None, }, { "key": "z.errors.ThrottledError", "category": "errors", "title": "Rate limit — retry after delay", "signature": "throw new z.errors.ThrottledError(message, delaySeconds?)", "high_level": "Signals 429. Pass delay from Retry-After.", "internals": "Zapier backs off for delaySeconds then retries the perform. Also thrown by afterResponse throw-for-throttling middleware.", "inputs": "```ts\n(message: string, delaySeconds?: number)\n```", "outputs": "```ts\nnever\n```", "related": ["z.request"], "examples": [{"title": "Honor Retry-After", "code": "throw new z.errors.ThrottledError('slow down', Number(resp.getHeader('retry-after') || 60));"}], "mcp_twin": None, "sdk_twin": "ZAPIER_ACTION_ERROR … N) Retry-After on run-action", }, { "key": "z.errors.ResponseError", "category": "errors", "title": "Wrap an HTTP response as an error", "signature": "throw new z.errors.ResponseError(response)", "high_level": "Usually auto-thrown by z.request on non-2xx.", "internals": "JSON-stringifies status, content-type, retry-after, content, request URL.", "inputs": "```ts\n(response: HttpResponse)\n```", "outputs": "```ts\nnever\n```", "related": ["z.request"], "examples": [{"title": "Manual throw", "code": "if (resp.status >= 500) throw new z.errors.ResponseError(resp);"}], "mcp_twin": None, "sdk_twin": None, }, { "key": "z.errors.CheckError", "category": "errors", "title": "Platform check failed", "signature": "throw new z.errors.CheckError(message?)", "high_level": "Raised by runtime checks (trigger not array, create not object, missing id).", "internals": "See packages/core/src/checks/*. You rarely throw this yourself; fix the return shape instead.", "inputs": "```ts\n(message?: string)\n```", "outputs": "```ts\nnever\n```", "related": ["PollingTriggerPerform", "CreatePerform"], "examples": [], "mcp_twin": None, "sdk_twin": None, }, { "key": "z.errors.StopRequestError", "category": "errors", "title": "Abort an outbound HTTP request from middleware", "signature": "throw new z.errors.StopRequestError(message?)", "high_level": "Throw from beforeRequest to cancel z.request without treating it as an app failure.", "internals": "Short-circuits the HTTP stack. Used when middleware decides the call should not go out.", "inputs": "```ts\n(message?: string)\n```", "outputs": "```ts\nnever\n```", "related": ["BeforeRequestMiddleware", "z.request"], "examples": [{"title": "Skip empty search", "code": "if (!request.params.q) throw new z.errors.StopRequestError('no query');"}], "mcp_twin": None, "sdk_twin": None, }, { "key": "z.errors.DehydrateError", "category": "errors", "title": "Hydration pointer failed", "signature": "throw new z.errors.DehydrateError(message?)", "high_level": "Bad dehydrate token or hydrator threw.", "internals": "Runtime-raised when a hydrate pointer cannot be resolved.", "inputs": "```ts\n(message?: string)\n```", "outputs": "```ts\nnever\n```", "related": ["z.dehydrate"], "examples": [], "mcp_twin": None, "sdk_twin": None, }, { "key": "createAppTester", "category": "testing", "title": "Unit-test a perform or request template", "signature": "createAppTester(appRaw, options?) → (func|request, bundle?) => Promise", "high_level": "From zapier-platform-core. Invokes a perform with a partial bundle.", "internals": "Builds a local z + bundle, runs the same execute path as production (minus some hook/file limits). Used by `zapier-platform test`.", "inputs": "```ts\n(appRaw: object, options?: { customStoreKey?: string }) => AppTester\n```", "outputs": "```ts\n(func | Request, bundle?: DeepPartial) => Promise\n```", "related": ["zapier.tools.env.inject", "zapier-platform test", "zapier-platform invoke"], "examples": [{"title": "Jest", "code": "const tester = createAppTester(App);\nconst rows = await tester(App.triggers.new_contact.operation.perform, {\n authData: { api_key: process.env.API_KEY },\n inputData: {},\n});"}], "mcp_twin": None, "sdk_twin": None, }, { "key": "zapier.tools.env.inject", "category": "testing", "title": "Load .env into process.env", "signature": "zapier.tools.env.inject(filename?)", "high_level": "Called automatically by the test runner. Useful in custom setup.", "internals": "Parses KEY=VAL lines. Auth fields for invoke live as authData_* in .env.", "inputs": "```ts\n(filename?: string) => void\n```", "outputs": "```ts\nvoid\n```", "related": ["createAppTester", "zapier-platform invoke"], "examples": [{"title": "Manual", "code": "require('zapier-platform-core').tools.env.inject();"}], "mcp_twin": None, "sdk_twin": None, }, { "key": "BeforeRequestMiddleware", "category": "middleware", "title": "Mutate every outbound z.request", "signature": "(request, z, bundle) => request | Promise", "high_level": "App.beforeRequest. Typical: attach Bearer from bundle.authData.", "internals": "Runs in order before fetch. Must return the request object (with url).", "inputs": "```ts\n(request: HttpRequestOptions & { url: string }, z: ZObject, bundle: Bundle) => request | Promise\n```", "outputs": "```ts\nHttpRequestOptions & { url: string }\n```", "related": ["AfterResponseMiddleware", "z.request"], "examples": [{"title": "Bearer", "code": "const addAuth = (req, z, bundle) => {\n req.headers = req.headers || {};\n req.headers.Authorization = `Bearer ${bundle.authData.access_token}`;\n return req;\n};\nmodule.exports = { beforeRequest: [addAuth] };"}], "mcp_twin": None, "sdk_twin": None, }, { "key": "AfterResponseMiddleware", "category": "middleware", "title": "Inspect every HTTP response", "signature": "(response, z, bundle) => response | Promise", "high_level": "App.afterResponse. Throw RefreshAuthError on 401; remap error envelopes.", "internals": "Runs after fetch, before the perform sees the response. Can mutate response.data.", "inputs": "```ts\n(response: HttpResponse, z: ZObject, bundle: Bundle) => response | Promise\n```", "outputs": "```ts\nHttpResponse\n```", "related": ["BeforeRequestMiddleware", "z.errors.RefreshAuthError"], "examples": [{"title": "401 → refresh", "code": "const maybeRefresh = (resp, z) => {\n if (resp.status === 401) throw new z.errors.RefreshAuthError();\n return resp;\n};"}], "mcp_twin": None, "sdk_twin": None, }, { "key": "PollingTriggerPerform", "category": "perform", "title": "Polling trigger perform", "signature": "(z, bundle) => object[] | Promise", "high_level": "Must return an array of objects with id (or primary output fields).", "internals": "Zapier dedupes by id. Newest-first recommended. meta.isPopulatingDedupe / isLoadingSample / isFillingDynamicDropdown change how many you should fetch.", "inputs": "```ts\n(z: ZObject, bundle: Bundle) => object[] | Promise\n```", "outputs": "```ts\nArray<{ id: string } & Record>\n```", "related": ["z.cursor.get", "z.request", "TriggerSchema"], "examples": [{"title": "List contacts", "code": "const r = await z.request('https://api.example.com/contacts');\nreturn r.data.map((c) => ({ id: c.id, name: c.name }));"}], "mcp_twin": None, "sdk_twin": "list-triggers / create-trigger-inbox", }, { "key": "WebhookTriggerPerform", "category": "perform", "title": "REST Hook perform", "signature": "(z, bundle) => object[] | Promise", "high_level": "Parse bundle.cleanedRequest into an array of objects.", "internals": "Also implement performSubscribe / performUnsubscribe / performList. subscribe stores bundle.subscribeData; unsubscribe uses it. targetUrl is the Zapier hook URL to register with the partner.", "inputs": "```ts\n(z: ZObject, bundle: Bundle) => object[] | Promise\n```", "outputs": "```ts\nobject[]\n```", "related": ["WebhookTriggerPerformSubscribe", "BasicHookOperationSchema"], "examples": [{"title": "Unwrap", "code": "const evt = bundle.cleanedRequest;\nreturn [{ id: evt.id, ...evt.data }];"}], "mcp_twin": None, "sdk_twin": "create-trigger-inbox (hosted subscription)", }, { "key": "CreatePerform", "category": "perform", "title": "Create action perform", "signature": "(z, bundle) => object | Promise", "high_level": "Must return one object, not an array.", "internals": "Returning a non-object fails 'non-object from create'. Use performResume + z.generateCallbackUrl for long jobs. performBuffer for bulk.", "inputs": "```ts\n(z: ZObject, bundle: Bundle) => object | Promise\n```", "outputs": "```ts\nobject\n```", "related": ["z.request", "z.generateCallbackUrl", "performBuffer"], "examples": [{"title": "Create contact", "code": "const r = await z.request({ url: '.../contacts', method: 'POST', json: bundle.inputData });\nreturn r.data;"}], "mcp_twin": "execute_zapier_write_action", "sdk_twin": "run-action write ", }, { "key": "SearchPerform", "category": "perform", "title": "Search action perform", "signature": "(z, bundle) => object[] | Promise", "high_level": "Must return an array (possibly empty). Empty = not found (needed for search-or-create).", "internals": "May return { results, paging_token } envelope. meta.paging_token continues a previous page.", "inputs": "```ts\n(z: ZObject, bundle: Bundle) => object[] | Promise\n```", "outputs": "```ts\nobject[] | { results: object[]; paging_token?: string }\n```", "related": ["CreatePerform", "SearchOrCreateSchema"], "examples": [{"title": "Find by email", "code": "const r = await z.request({ url: '.../contacts', params: { email: bundle.inputData.email } });\nreturn r.data.items || [];"}], "mcp_twin": "execute_zapier_read_action", "sdk_twin": "run-action search ", }, { "key": "performBuffer", "category": "perform", "title": "Buffered / bulk create", "signature": "(z, BufferedBundle) => Promise<{[id]: {outputData?, error?}}>", "high_level": "Process bundle.buffer items in one API call. Return a map keyed by each item's meta.id.", "internals": "Zapier groups creates and sends them together. Each result must be success (outputData) or error string.", "inputs": "```ts\n(z: ZObject, bundle: { authData; buffer: { inputData; meta: { id: string } }[]; groupedBy }) => Promise\n```", "outputs": "```ts\nRecord\n```", "related": ["CreatePerform"], "examples": [{"title": "Bulk insert", "code": "const created = await postAll(bundle.buffer.map((i) => i.inputData));\nreturn Object.fromEntries(bundle.buffer.map((i, n) => [i.meta.id, { outputData: created[n] }]));"}], "mcp_twin": None, "sdk_twin": None, }, ] # --------------------------------------------------------------------------- # CLI / SDK parsers # --------------------------------------------------------------------------- CLI_INTERNALS = { "init": "Copies an official example-apps template (oauth2, session-auth, …) into PATH. Does not call Zapier until register/push.", "scaffold": "AST-edits index.js/ts to register a new trigger/search/create/resource file generated from packages/cli/scaffold templates.", "convert": "Downloads a Visual Builder definition (or --json) and emits CLI source. Existing files are not clobbered.", "validate": "Runs zapier-platform-schema JSON Schema plus optional live style checks against Zapier's servers.", "build": "Temp copy → zapierwrapper.js entry → esbuild dep detection → build/build.zip + source.zip.", "upload": "POSTs build.zip + source.zip for the version in package.json. Versions must be sequential.", "push": "build then upload. --snapshot makes 0.0.0-LABEL for dev.", "invoke": "Local (default, .env), relay (-a auth id, traffic via Zapier), or remote (-r, production). Emulates (z, bundle).", "test": "Wrapper around npm/yarn/pnpm test after validate + env inject.", "register": "Creates the integration on developer.zapier.com and writes .zapierapprc.", "link": "Writes .zapierapprc pointing at an existing integration id.", "login": "Stores a deploy key in ~/.zapierrc (SSO via --sso).", "promote": "Marks a pushed version as the public default. Does not migrate users.", "migrate": "Moves users FROM→TO (optional percent). Non-breaking only. Track with jobs.", "deprecate": "Schedules removal (DATE ≥ 3 weeks). Users emailed at T-14d.", "canary:create": "Temporary traffic split FROM→TO for duration seconds. Reverts when expired.", } SDK_CATEGORY_INTERNALS = { "Accounts": "Browser OAuth to Zapier itself (not a third-party app). Writes SDK credentials used by createZapierSdk().", "Apps": "Directory of 9,000+ integrations. Same catalog as MCP discover_zapier_actions.", "Connections": "OAuth grants Zapier holds per app+user. MCP twins: list/manage_zapier_connections.", "Actions": "Discover and run partner actions. MCP twins: inspect + execute_zapier_*_action.", "Triggers": "Trigger Inbox API — subscribe to partner events, lease/ack messages.", "Tables": "Zapier Tables CRUD (not partner apps).", "HTTP Requests": "Authenticated raw HTTP through a connection (SDK fetch / curl).", "Code Workflows": "Experimental durable workflows on Zapier infrastructure.", "Client Credentials": "Deploy SDK in CI without a browser login.", "Utilities": "Project init, type generation, local MCP server for the SDK.", } def parse_platform_cli(text: str) -> list[dict]: parts = re.split(r"\n## ", text) rows = [] for part in parts[1:]: lines = part.splitlines() name = lines[0].strip() block = "\n".join(lines[1:]).strip() usage = "" m = re.search(r"\*\*Usage\*\*:\s*`([^`]+)`", block) if m: usage = m.group(1) summary = "" qm = re.search(r"^>\s*(.+)$", block, re.M) if qm: summary = qm.group(1).strip() aliases = [] am = re.search(r"\*\*Aliases\*\*\s*((?:\n\* .*)+)", block) if am: aliases = re.findall(r"`([^`]+)`", am.group(1)) flags = [] for fl in re.findall(r"^\* (.+)$", block, re.M): if "`" in fl or fl.startswith("(required)") or "--" in fl: flags.append(fl.strip()) examples = re.findall(r"^\* (`[^`]+`)", block, re.M) examples = [e.strip("`") for e in examples] args = [] in_args = False for line in lines: if line.startswith("**Arguments**"): in_args = True continue if in_args: if line.startswith("**"): break if line.startswith("* "): args.append(line[2:].strip()) rows.append( { "name": name, "usage": usage or f"zapier-platform {name}", "summary": summary, "block": block, "flags": flags, "args": args, "examples": examples, "aliases": aliases + [f"zapier {name}"], } ) return rows def parse_sdk_cli(text: str) -> list[dict]: rows = [] category = "Utilities" # headings: ### `get-profile` or #### `get-profile` chunks = re.split(r"\n#{3,4} `([^`]+)`", text) # chunks[0] preamble, then name, body, name, body... intro = chunks[0] cats = re.findall(r"^### ([A-Za-z ]+)$", intro, re.M) for i in range(1, len(chunks), 2): name = chunks[i].strip() body = chunks[i + 1] if i + 1 < len(chunks) else "" # category: last ### Title before this that is NOT a command # infer from inventory or from nearby ### without backticks cat_m = re.search(r"experimental", body[:80], re.I) usage = "" um = re.search(r"Usage:\s*zapier-sdk ([^\n]+)", body) if um: usage = "zapier-sdk " + um.group(1).strip() summary = "" # first non-empty prose line after heading extras for line in body.splitlines(): s = line.strip() if not s or s.startswith("Usage:") or s.startswith("```") or s.startswith("_(requires"): continue if s.startswith("Arguments:") or s.startswith("Options:"): break if s.startswith("(") and "experimental" in s: continue summary = s break args = [] opts = [] section = None for line in body.splitlines(): if line.startswith("Arguments:"): section = "args" continue if line.startswith("Options:"): section = "opts" continue if section and line.strip() and not line.startswith(" ") and not line.startswith("-") and line[0].isalpha() and ":" not in line[:20]: # new section heading if line.strip() in {"Arguments:", "Options:"}: continue if section == "args" and re.match(r"^\s{2}\S", line): args.append(line.strip()) if section == "opts" and re.match(r"^\s{2}-", line): opts.append(line.strip()) # category from earlier inventory headings in file rows.append( { "name": name, "usage": usage or f"zapier-sdk {name}", "summary": summary, "block": body.strip(), "args": args, "flags": opts, "examples": re.findall(r"zapier-sdk [^\n`]+", body)[:6], "experimental": "experimental" in body[:400].lower() or "requires `--experimental`" in body, "ts": kebab_to_camel(name), } ) # assign categories from the inventory section cat_map = {} current = "Utilities" for line in text.splitlines(): m = re.match(r"^### ([A-Za-z ]+)$", line) if m and not line.startswith("### `"): current = m.group(1).strip() cm = re.match(r"^- `([a-z0-9.:-]+)", line) if cm: cat_map[cm.group(1)] = current for r in rows: r["category"] = cat_map.get(r["name"], "Utilities") return rows def render_body(kind_label: str, rec: dict, internals: str, related: list[str], twins: dict, typed: str, outputs: str, examples_md: str) -> str: parts = [ f"# `{rec['name']}`", "", f"> {rec.get('summary') or rec['name']}", "", "## High-level description", "", rec.get("summary") or rec["name"], "", "## Internals", "", internals, "", "## Typed inputs", "", typed, "", "## Outputs", "", outputs, "", f"## Surface: {kind_label}", "", f"- Usage: `{rec.get('usage') or rec['name']}`", ] if rec.get("aliases"): parts.append("- Aliases: " + ", ".join(f"`{a}`" for a in rec["aliases"])) if rec.get("ts"): parts.append(f"- TypeScript: `zapier.{rec['ts']}(...)`") if rec.get("experimental"): parts.append("- Experimental: requires `--experimental`") parts += ["", "## Related functions", ""] for rel in related: parts.append(f"- `{rel}`") if not related: parts.append("- _(see same category)_") parts += ["", "## Twins (MCP / other CLI)", ""] for k, v in twins.items(): if v: parts.append(f"- **{k}:** `{v}`") if rec.get("block"): parts += ["", "## Official text", "", rec["block"][:8000]] if examples_md: parts += ["", "## Examples", "", examples_md] return "\n".join(parts) def typed_from_cli(rec: dict) -> str: lines = ["```ts", "type Input = {"] for a in rec.get("args") or []: name = a.split("|")[0].strip().split()[0].strip("`()") req = "(required)" in a or a.lower().startswith("(required)") desc = a.split("|", 1)[-1].strip() if "|" in a else a lines.append(f" {name}{'?' if not req else ''}: string; // {desc[:140]}") for fl in rec.get("flags") or []: names = re.findall(r"`([^`]+)`", fl) flag = names[0] if names else fl.split()[0] flag = flag.lstrip("-") if not flag or flag in {"d", "h", "help"}: # still include long flags longf = next((n.lstrip("-") for n in names if n.startswith("--")), flag) flag = longf or flag lines.append(f" {flag.replace('-', '_') }?: string | boolean; // {fl[:140]}") if len(lines) == 2: lines.append(" // see official flags") lines += ["};", "```"] return "\n".join(lines) def examples_md(examples: list[str]) -> str: if not examples: return "" return "\n".join(f"```bash\n{e}\n```" for e in examples[:8]) def core_row(item: dict) -> dict: examples_md_s = "\n\n".join( f"### {e['title']}\n\n```ts\n{e['code']}\n```" for e in item.get("examples") or [] ) body = "\n".join( [ f"# `{item['key']}`", "", f"> {item['high_level']}", "", "## High-level description", "", item["high_level"], "", "## Internals", "", item["internals"], "", "## Typed inputs", "", item["inputs"], "", "## Outputs", "", item["outputs"], "", f"- Signature: `{item['signature']}`", f"- Category: `{item['category']}`", "", "## Related functions", "", *([f"- `{r}`" for r in item.get("related") or []] or ["- —"]), "", "## Twins (MCP / SDK)", "", f"- MCP: `{item.get('mcp_twin') or '—'}`", f"- SDK: `{item.get('sdk_twin') or '—'}`", "", "## Examples", "", examples_md_s or "_see signature_", ] ) return { "_id": f"core_function:{item['key'].lower()}", "kind": "core_function", "key": item["key"], "title": f"{item['key']} — {item['title']}", "summary": item["high_level"], "body": body, "usage": item["signature"].split("\n")[0], "signature": item["signature"].split("\n")[0], "aliases": [], "flags": [], "args": [], "examples": item.get("examples") or [], "source_url": "https://docs.zapier.com/integrations/build-cli/core", "source_repo": "zapier/zapier-platform", "source_path": "packages/core/types/custom.d.ts", "section": "platform-core", "tags": ["core", "function", item["category"], "zapier-platform-core"], "related": item.get("related") or [], "meta": { "surface": "platform_core", "category": item["category"], "mcp_twin": item.get("mcp_twin"), "sdk_twin": item.get("sdk_twin"), "internals": item["internals"], }, "ingested_at": NOW, } def main() -> int: rows: list[dict] = [] # core for item in CORE: rows.append(core_row(item)) # platform CLI if PLATFORM_CLI_MD.exists(): cli = parse_platform_cli(PLATFORM_CLI_MD.read_text(encoding="utf-8", errors="replace")) else: print("missing platform cli.md", file=sys.stderr) cli = [] for rec in cli: internals = CLI_INTERNALS.get( rec["name"], "Platform CLI talks to developer.zapier.com with the deploy key in ~/.zapierrc. " "Implementation: zapier-platform/packages/cli/src/oclif/commands/.", ) related = [] if rec["name"] in ("push", "upload", "build"): related = ["build", "upload", "push", "validate"] elif rec["name"] in ("promote", "migrate", "deprecate", "canary:create", "jobs"): related = ["promote", "migrate", "deprecate", "jobs", "versions"] elif rec["name"].startswith("invoke"): related = ["invoke", "test", "createAppTester"] elif rec["name"] in ("init", "scaffold", "convert"): related = ["init", "scaffold", "convert", "register"] twins = { "deprecated alias": f"zapier {rec['name']}", "MCP": "n/a (this builds integrations, MCP consumes them)", } body = render_body( "zapier-platform CLI (build integrations)", rec, internals, related, twins, typed_from_cli(rec), "```ts\n// stdout (human table by default). --format json|raw|table|row|plain\n```", examples_md(rec.get("examples") or []), ) doc = { "_id": f"cli_function:{rec['name']}", "kind": "cli_function", "key": rec["name"], "title": f"zapier-platform {rec['name']}", "summary": rec.get("summary") or rec["name"], "body": body, "usage": rec.get("usage"), "signature": rec.get("usage"), "aliases": rec.get("aliases") or [], "flags": rec.get("flags") or [], "args": rec.get("args") or [], "examples": rec.get("examples") or [], "source_url": "https://github.com/zapier/zapier-platform/blob/main/packages/cli/docs/cli.md", "source_repo": "zapier/zapier-platform", "source_path": "packages/cli/docs/cli.md", "section": "platform-cli", "tags": ["cli", "function", "zapier-platform-cli", rec["name"].split(":")[0]], "related": related, "meta": {"surface": "platform_cli", "internals": internals}, "ingested_at": NOW, } rows.append(doc) # keep legacy kind in sync legacy = dict(doc) legacy["_id"] = f"cli_command:{rec['name']}" legacy["kind"] = "cli_command" rows.append(legacy) # SDK CLI + TS methods if SDK_CLI_MD.exists(): sdk = parse_sdk_cli(SDK_CLI_MD.read_text(encoding="utf-8", errors="replace")) else: print("missing sdk cli-commands.md", file=sys.stderr) sdk = [] MCP_TWINS = { "list-apps": "discover_zapier_actions", "get-app": "discover_zapier_actions", "list-actions": "inspect_zapier_actions / discover_zapier_actions", "get-action": "inspect_zapier_actions", "list-action-input-fields": "inspect_zapier_actions", "get-action-input-fields-schema": "inspect_zapier_actions", "list-action-input-field-choices": "inspect_zapier_actions (enum_property)", "run-action": "execute_zapier_read_action / execute_zapier_write_action", "list-connections": "list_zapier_connections", "find-first-connection": "list_zapier_connections", "find-unique-connection": "list_zapier_connections", "create-connection": "manage_zapier_connections", "get-connection-start-url": "manage_zapier_connections", "wait-for-new-connection": "manage_zapier_connections (after auth_url)", "curl": "write_code_action / z.request analog", "mcp": "hosted Zapier MCP (different server)", "feedback": "send_feedback", } for rec in sdk: cat = rec.get("category") or "Utilities" internals = SDK_CATEGORY_INTERNALS.get(cat, "Zapier SDK talks to api.zapier.com with SDK credentials.") internals += f" TypeScript method: `zapier.{rec['ts']}`. CLI: `{rec['usage']}`." related = [x["name"] for x in sdk if x.get("category") == cat and x["name"] != rec["name"]][:8] twins = { "TypeScript": f"zapier.{rec['ts']}()", "MCP": MCP_TWINS.get(rec["name"], "—"), "Platform CLI": "n/a (SDK consumes apps; Platform CLI publishes them)", } typed = typed_from_cli(rec) # better TS blurb typed = f"```ts\n// CLI: {rec['usage']}\n// TS: const {{ data }} = await zapier.{rec['ts']}({{ ... }})\n```\n" + typed body = render_body( f"Zapier SDK CLI / TypeScript ({cat})", rec, internals, related, twins, typed, "```ts\n{ data: T } | paginated { data: T[]; next_cursor?: string }\n// --json for raw. Always prefer live discovery over hardcoded keys.\n```", examples_md(rec.get("examples") or [rec["usage"]]), ) doc = { "_id": f"sdk_function:{rec['name']}", "kind": "sdk_function", "key": rec["name"], "title": f"zapier-sdk {rec['name']} / zapier.{rec['ts']}", "summary": rec.get("summary") or rec["name"], "body": body, "usage": rec.get("usage"), "signature": f"zapier.{rec['ts']}()", "aliases": [rec["ts"], f"zapier.{rec['ts']}"], "flags": rec.get("flags") or [], "args": rec.get("args") or [], "examples": rec.get("examples") or [], "source_url": "https://docs.zapier.com/sdk/cli-reference", "source_repo": "zapier/sdk", "source_path": "skills/zapier-sdk/references/cli-commands.md", "section": "sdk", "tags": ["sdk", "function", "zapier-sdk", cat.lower().replace(" ", "-")], "related": related, "meta": { "surface": "sdk", "category": cat, "typescript": rec["ts"], "experimental": rec.get("experimental"), "mcp_twin": MCP_TWINS.get(rec["name"]), "internals": internals, }, "ingested_at": NOW, } rows.append(doc) # also write camelCase lookup aliases as thin pointers? skip — aliases field is enough guide_body = f"""# Zapier functions — complete reference (CLI, core, SDK) **Start here first:** `db.platform_reference.findOne({{ kind: "guide", key: "build-new-connector" }})` or [PLATFORM-REFERENCE.md](PLATFORM-REFERENCE.md). This guide is the **function catalog**. MCP tools are documented separately as `kind: "mcp_function"` and [MCP-REFERENCE.md](MCP-REFERENCE.md). ## Surfaces (do not mix) | Surface | kind | Count | Package | |---------|------|------:|---------| | Hosted MCP meta-tools | `mcp_function` | 17 | mcp.zapier.com | | Platform CLI (publish an integration) | `cli_function` | {sum(1 for r in rows if r['kind']=='cli_function')} | `zapier-platform-cli` | | Platform core (`z`, perform, middleware) | `core_function` | {sum(1 for r in rows if r['kind']=='core_function')} | `zapier-platform-core` | | SDK CLI + TypeScript (consume apps) | `sdk_function` | {sum(1 for r in rows if r['kind']=='sdk_function')} | `@zapier/zapier-sdk` / `@zapier/zapier-sdk-cli` | ```js db.platform_reference.find({{ kind: "cli_function" }}).sort({{ key: 1 }}) db.platform_reference.find({{ kind: "core_function" }}).sort({{ key: 1 }}) db.platform_reference.find({{ kind: "sdk_function" }}).sort({{ key: 1 }}) db.platform_reference.findOne({{ kind: "sdk_function", key: "run-action" }}) db.platform_reference.findOne({{ kind: "core_function", key: "z.request" }}) ``` Human index: [FUNCTIONS-REFERENCE.md](FUNCTIONS-REFERENCE.md). ## Build-an-integration call graph ``` zapier-platform login → init --template oauth2 → scaffold trigger|create|search → implement (z, bundle) with z.request / z.errors.* → invoke auth start|test / test / validate → register → push → promote → migrate ``` ## Consume-an-app call graph (SDK) ``` zapier-sdk login → list-apps → list-actions → list-action-input-fields → find-first-connection | create-connection → run-action (events) list-triggers → create-trigger-inbox → lease/ack (raw HTTP) curl --connection ``` Same methods exist on `createZapierSdk()` as camelCase (`runAction`, `listApps`, …). """ rows.append( { "_id": "guide:zapier-functions", "kind": "guide", "key": "zapier-functions", "title": "Zapier functions — CLI, core, SDK catalog", "summary": "Complete function reference. Start from build-new-connector, then query cli_function / core_function / sdk_function.", "body": guide_body, "usage": 'db.platform_reference.findOne({kind:"guide", key:"zapier-functions"})', "signature": "", "aliases": ["functions playbook"], "flags": [], "args": [], "examples": [], "source_url": "", "source_repo": "", "source_path": "scripts/ingest-function-reference.py", "section": "guide", "tags": ["guide", "functions", "cli", "sdk", "core"], "related": [], "meta": { "cli": sum(1 for r in rows if r["kind"] == "cli_function"), "core": sum(1 for r in rows if r["kind"] == "core_function"), "sdk": sum(1 for r in rows if r["kind"] == "sdk_function"), }, "ingested_at": NOW, } ) # markdown index md = [ "# Zapier function reference (CLI, core, SDK)", "", "**Grok start:** `db.platform_reference.findOne({ kind: \"guide\", key: \"build-new-connector\" })` ", "MCP tools: [MCP-REFERENCE.md](MCP-REFERENCE.md). This file is everything else.", "", "Each function has a Mongo document with the same sections as MCP: high-level, internals, typed inputs, outputs, related, twins, examples.", "", "## Core (`kind: \"core_function\"`)", "", "| Function | Signature | Category |", "|----------|-----------|----------|", ] for r in sorted((x for x in rows if x["kind"] == "core_function"), key=lambda x: x["key"]): md.append(f"| `{r['key']}` | `{r['signature']}` | {r['meta'].get('category','')} |") md += ["", "## Platform CLI (`kind: \"cli_function\"`)", "", "| Command | Usage |", "|---------|-------|"] for r in sorted((x for x in rows if x["kind"] == "cli_function"), key=lambda x: x["key"]): md.append(f"| `{r['key']}` | `{r['usage']}` |") md += ["", "## SDK (`kind: \"sdk_function\"`)", "", "| CLI | TypeScript | Category | MCP twin |", "|-----|------------|----------|----------|"] for r in sorted((x for x in rows if x["kind"] == "sdk_function"), key=lambda x: (x["meta"].get("category",""), x["key"])): md.append( f"| `{r['key']}` | `zapier.{r['meta'].get('typescript')}` | {r['meta'].get('category')} | {r['meta'].get('mcp_twin') or '—'} |" ) md += [ "", "## Query", "", "```js", 'db.platform_reference.findOne({ kind: "guide", key: "zapier-functions" })', 'db.platform_reference.find({ kind: "core_function", key: "z.request" })', 'db.platform_reference.find({ kind: "sdk_function", "meta.category": "Actions" })', "```", "", ] md_path = ROOT / "FUNCTIONS-REFERENCE.md" md_path.write_text("\n".join(md) + "\n") print(f"wrote {md_path}") out = ROOT / "raw" / "function-reference.jsonl" out.parent.mkdir(parents=True, exist_ok=True) with out.open("w") as f: for r in rows: f.write(json.dumps(r, ensure_ascii=False) + "\n") counts: dict[str, int] = {} for r in rows: counts[r["kind"]] = counts.get(r["kind"], 0) + 1 print("wrote", len(rows), "→", out, counts) uri = os.environ.get("ZAPIER_MONGO_URI") or os.environ.get("MDB_MCP_CONNECTION_STRING") if not uri: print("ZAPIER_MONGO_URI unset; skipped mongo") return 0 from pymongo import MongoClient, ReplaceOne col = MongoClient(uri).get_database("zapier")["platform_reference"] BATCH = 200 ops = [ReplaceOne({"_id": r["_id"]}, r, upsert=True) for r in rows] for i in range(0, len(ops), BATCH): col.bulk_write(ops[i : i + BATCH], ordered=False) col.create_index([("kind", 1), ("key", 1)]) print( "mongo", {k: col.count_documents({"kind": k}) for k in ("cli_function", "core_function", "sdk_function", "mcp_function", "guide")}, ) return 0 if __name__ == "__main__": raise SystemExit(main())