← Zapier Release
API
Tokens

Run Zapier Release from your own code

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page parses both definitions (reading perform code as structure, never running it), builds the step table, runs every design and publish check, classifies every change as additive, hide or breaking, lists what only a hand diff can show, computes the next version, ranks the auth ladder, decides each trigger's type, ranks the grow levers and lays out the publish timeline, then hands it all to the run as facts. The run plans the audit and publication or the release; the page then holds it to its own facts (zpreply.js). Zapier Release is not affiliated with or endorsed by Zapier.

Base URL and the response shape

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }

The token is minted for this app (the guest call takes {"slug":"zapier-release"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required key (task, integration, definition, jobs, facts) is missing. The page always sends all nine of task, integration, definition, previous, diff, jobs, evidence, carry, facts; carry is an empty string for plan, and previous and diff are empty strings when no previous version was pasted.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: both lanes are metered and need a personal sign-in.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Two lanes, chosen by task, in the order the work runs - the audit, then the release of a change:

A missing or unknown task is answered as plan. Both lanes send the same nine fields. The facts are the page's checks, one row per line, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the plan's step classes and fixes into carry for the release lane.

The input

FieldRequiredWhat
taskyesplan or release. If it is missing or neither, do plan and set "task": "plan".
integrationyesthe interview answers: the name, the stage, the tooling, live users on this version, users with a live Zap, the date a result must land by with its days from today, engineering days, how breaking changes ship (as pasted, a new major, or as twins), whether the old version must stop working, the API capabilities, the integrations directory and the publish facts.
definitionyesthe definition as the page read it: version, platformVersion, authentication, and one line per step (key, label, type, visibility, inputs, outputs, sample, description). No perform source.
previoussend it (empty with no previous version; release needs it)the previous version read the same way, or empty.
diffsend it (empty with no previous version)the page's change diff, one row per change (step, change, class, ship, detail, consequence, the additive alternative), then the diff-by-hand list, the Zap templates to rebuild and the target version; or empty.
jobsyes (says so when none were given)the customer jobs, one per line.
evidencesend itusers with a live Zap, the live-run evidence per visible step and the validation lines pasted.
carrysend it (empty for plan)release task only: the audit plan's step classes and fixes when the plan ran first; may be empty.
factsyesthe page's checks, one line each with the rule, the id, the status and the evidence separated by vertical bars; then the route, the step classes, the auth ladder, the trigger types, the grow levers, the publish timeline by week, the version rule, the figure list and the footnote. A line starting "must:" applies to your reply.
retry_notenooptional; if present, your previous reply was unusable - follow it and reply with only the JSON object.

The rules the model applies

From the source skill, @samber/zapier-integration-playbook - the only standard the run applies:

House rules

The replies

plan

Audit the integration and plan its publication: the route, the step table, the auth and trigger-type decisions, one fix per failing check, the publish checklist by week and the grow levers.

  1. summary - one sentence, at most 40 words: what stands between the integration and a clean publish or release today, and what the plan changes.
  2. route - exactly the facts' route.
  3. steps - one object per definition step, in the facts' order: key (exactly as given), label, type (exactly the facts' type: trigger polling, trigger hook, create, search or searchOrCreate), class (exactly the facts' class: keep, additive, hide, breaking or rejected), reason (one sentence).
  4. auth - an object: decision (exactly the facts' pick: oauth2, api_key, session, basic or digest), rests_on (the capability the facts name, quoted exactly, and what the input says about it).
  5. trigger_types - one object per visible trigger: key, decision (exactly the facts' decision: polling or hook), rests_on (quote "webhook subscription create/delete per Zap" and what the input says about it).
  6. fixes - one object per check the facts mark fail, and any warn worth fixing: check (the page's check id), change (one concrete sentence: what to change in the definition, the API, the evidence or the publish facts). Every fail check appears.
  7. publish - the publish checklist, one object per item: item (one sentence), owner (a role, one of engineering, product, partnerships, support, marketing, integration admin - never a person), week (a week number from the facts' timeline).
  8. grow - one object per lever, in the facts' order: lever (exactly the facts' lever id: templates, launch_embed, workflow_element, onboarding, spotlight or workflow_api), why (one sentence naming the answer that moved it, or why it sits where it does).
  9. footnote - exactly the footnote.
  10. caveats - at most 4 short strings: what the team must confirm; [] when there is nothing to say.
{
  "task": "plan",
  "summary": "one sentence",
  "route": "Design, Build, then Publish",
  "steps": [ { "key": "new_invoice", "label": "New Invoice", "type": "trigger polling", "class": "keep", "reason": "one sentence" } ],
  "auth": { "decision": "oauth2", "rests_on": "OAuth issued by the product: yes" },
  "trigger_types": [ { "key": "new_invoice", "decision": "polling", "rests_on": "webhook subscription create/delete per Zap: no" } ],
  "fixes": [ { "check": "sample_present.find_invoice", "change": "one sentence" } ],
  "publish": [ { "item": "one sentence", "owner": "engineering", "week": 1 } ],
  "grow": [ { "lever": "templates", "why": "one sentence" } ],
  "footnote": "† verify against Zapier's live docs before quoting it.",
  "caveats": [ ]
}

The page holds it to its own facts (check() in zpreply.js; any warning makes the reply flagged): the reply answers the task it was sent; no URL with a scheme; no backticks; no implied Zapier endorsement; no promised delisting threshold; a summary of at most 40 words; every Zapier platform number with its dagger, and the exact footnote; the page's route (put back if moved); every definition step exactly once and no invented step, each with the page's type and class (a moved one is put back); the page's auth pick (put back) resting on the page's capability; one trigger decision per visible trigger with the page's decision (put back) resting on the subscription capability; a fix for every fail check and only the page's check ids; publish owners that are roles, in a week of the page's timeline; the grow levers in the page's order.

release

Plan the release of the changes between the previous version and this one: the target version, how each change ships, what to diff by hand, the canary, the migration, the Zap templates to rebuild and a short notice to users.

  1. summary - one sentence, at most 40 words: what changes for live Zaps and how the release keeps them running.
  2. target - an object: version (exactly the facts' target version), why (one sentence: the version rule behind it).
  3. changes - one object per diff row, in the facts' order: step (exactly as given), change (exactly the row's change id), class (exactly the row's class: additive, hide or breaking), ship (exactly the row's ship: twin, hide, major or keep - a breaking change never ships as keep), note (one sentence: what the team does for it).
  4. by_hand - one object per item of the facts' diff-by-hand list: what (the item exactly as the facts write it), how (one sentence: how to check it before promoting).
  5. canary - an object: how (CLI: a canary of 5-10%† of traffic for 30-60 minutes†, non-breaking changes only; Platform UI: share the clone with test accounts - from the input's tooling), watch (what to watch in Monitoring before migrating the rest).
  6. migrate - an object: when (one sentence: promote, then migrate - within the major only), deprecate (when the old version must stop working: a date 3 weeks† to 1 year† ahead, and Zapier emails affected users 14 days† before; otherwise exactly "mark legacy" with one clause of why).
  7. templates - the facts' list of Zap templates to rebuild, each string exactly as given; [] when the list is empty.
  8. notice - a short notice for in-app and email: what changed, then what users do, each item on its own line. Separate the lines with a newline, written \n inside the JSON string - for example "Version 1.1.0 adds a Find Task search.\nZaps you already run keep working.\nBuild new Zaps on New Task." Plain text; never promise Zapier will email everyone.
  9. footnote - exactly the footnote.
  10. caveats - at most 4 short strings: what to confirm before promoting; [] when there is nothing to say.
{
  "task": "release",
  "summary": "one sentence",
  "target": { "version": "1.1.0", "why": "one sentence" },
  "changes": [ { "step": "create_task", "change": "input_added_optional", "class": "additive", "ship": "keep", "note": "one sentence" } ],
  "by_hand": [ { "what": "The response structure of every step's live results", "how": "one sentence" } ],
  "canary": { "how": "one sentence", "watch": "one sentence" },
  "migrate": { "when": "one sentence", "deprecate": "mark legacy - one clause" },
  "templates": [ ],
  "notice": "first line\nsecond line",
  "footnote": "† verify against Zapier's live docs before quoting it.",
  "caveats": [ ]
}

The page requires: the reply answers the task it was sent; the same text rules; the page's target version (put back if moved); one change per diff row and no invented change, each with the page's class and ship (a moved one is put back; a breaking change shipped as keep is named); every by-hand item; a canary of 5-10%† for 30-60 minutes† with the CLI, or test accounts with the Platform UI; Monitoring; mark legacy, or a deprecation date 3 weeks† to 1 year† ahead with Zapier's email 14 days† before, as answered; exactly the page's Zap templates; a notice one item per line that never promises Zapier will email everyone.

Worked examples

Three runs from the page's free samples (invented products, hosts on the reserved .example domain): the audit plan for a public integration with breaking changes, its release plan carrying that audit, and the release plan for a clean additive version. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the long fields, strings and lists are shortened in this display only, and every cut is listed under the example. The last line is the page's own check of the full reply.

plan - A public integration with breaking changes

The page read it as: not yet - fails to fix first; stage public, CLI; steps new_invoice (breaking), new_payment (breaking), invoice_paid_webhook (hide), list_customers (keep), create_customer (keep), create_invoice (breaking), charge_card (rejected), find_customer (keep); fail checks: money.charge_card, hook_subscribe.new_payment, static_hook.invoice_paid_webhook, sample_present.find_customer.

Input (definition is 1423 characters; the first 1396 are shown; diff is 1955 characters; the first 1390 are shown; facts is 9306 characters; the first 1398 are shown):

{
 "task": "plan",
 "integration": "Integration: Ledgerwick; stage: public; tooling: CLI\nLive users on this version: 40; users with a live Zap: not given\nDate a result must land by: not given; engineering days available: 10\nBreaking changes ship: as pasted - a new major; the old version must stop working (an endpoint sunset): no\nAPI capabilities: webhook subscription create/delete per Zap: no; several subscriptions alive at once: no; newest-first sorting: yes; unique ids that never change format: yes; OAuth issued by the product: no; self-serve API keys: no; returns 401 on expired tokens: yes; Retry-After on 429: no\nRuns its own integrations directory with heavy search traffic: no\nPublish facts: A publicly launched product, not invite-only: yes; You own the API or hold the owner's written permission: yes; Production HTTPS endpoints only: yes; A non-expiring test account with paid features on: yes; An admin whose email domain matches the app's domain: yes; No other public integration for the app: no; The product does not compete with Zapier: no",
 "definition": "The definition as read: version 1.0.3; platformVersion 17.2.0\nAuthentication: basic; fields: none; test: yes; connection label: yes\nSteps (key | label | type | visibility | inputs | outputs | sample | description):\n- new_invoice | New Invoice | trigger polling | visible | inputs 0 | outputs 5 | sample yes | Triggers when a new invoice is created.\n- new_payment | New Payment | trigger hook | visible | inputs 0 | outputs 3 | sample yes | Triggers when a payment is recorded against an invoice.\n- invoice_paid_webhook | Invoice Paid Webhook | trigger hook (static webhook) | visible | inputs 0 | outputs 0 | sample yes | Triggers when an invoice is paid.\n- list_customers | List Customers | trigger polling | hidden | inputs 0 | outputs 0 | sample yes | Triggers when a customer is added. Powers the customer dropdown.\n- create_customer | Create Customer | create | visible | inputs 3 (required: name) | outputs 0 | sample yes | Creates a new customer.\n- create_invoice | Create Invoice | create | visible | inputs 4 (required: customer_id, amount, currency) | outputs 0 | sample yes | Creates a draft invoice for a customer.\n- charge_card | Charge Card | create | visible | inputs 2 (required: customer_id, amount) | outputs 0 | sample yes | Creates a charge on the customer's saved card.\n- find_customer | Find Customer | search | visible | inputs 1 (required: email) | outputs 0 | sample no |\n\n[... 27 more characters cut from this display ...]",
 "previous": "The previous version as read: version 1.0.3; platformVersion 17.2.0\nAuthentication: basic; fields: none; test: yes; connection label: yes\nSteps (key | label | type | visibility | inputs | outputs | sample | description):\n- new_invoice | New Invoice | trigger polling | visible | inputs 0 | outputs 5 | sample yes | Triggers when a new invoice is created.\n- new_payment | New Payment | trigger polling | visible | inputs 0 | outputs 3 | sample yes | Triggers when a payment is recorded against an invoice.\n- invoice_paid_webhook | Invoice Paid Webhook | trigger hook (static webhook) | visible | inputs 0 | outputs 0 | sample yes | Triggers when an invoice is paid.\n- list_customers | List Customers | trigger polling | hidden | inputs 0 | outputs 0 | sample yes | Triggers when a customer is added. Powers the customer dropdown.\n- create_customer | Create Customer | create | visible | inputs 3 (required: name) | outputs 0 | sample yes | Creates a new customer.\n- create_invoice | Create Invoice | create | visible | inputs 3 (required: customer_id, amount) | outputs 0 | sample yes | Creates a draft invoice for a customer.\n- find_customer | Find Customer | search | visible | inputs 1 (required: email) | outputs 0 | sample no | Finds a customer by email.",
 "diff": "The change diff (step | change | class | ship | detail | consequence for live Zaps | the additive alternative):\n- new_invoice | output_renamed | breaking | major | amount -> total | later steps mapped to amount get nothing - promotion does not block it | map the new API shape back to the old output in code, or twin it as new_invoice_v2 with the same name and description, make the change there, hide new_invoice; templates on a hidden step must be rebuilt and resubmitted\n- new_payment | poll_hook_switch | breaking | major | polling -> hook | a type change: live Zaps on new_payment stop (polling -> hook) | twin it as new_payment_v2 with the same name and description, make the change there, hide new_payment; templates on a hidden step must be rebuilt and resubmitted\n- create_invoice | input_required_added | breaking | major | currency | every live Zap lacks a value for currency and errors | give it a default, or twin it as create_invoice_v2 with the same name and description, make the change there, hide create_invoice; templates on a hidden step must be rebuilt and resubmitted\n- charge_card | step_added | additive | keep | create | live Zaps keep running (create) | none needed - additive\nDiff by hand (what the definitions cannot show):\n- The response structure of every step's live results - promotion compares schemas, not live results; it does not block a changed response\n\n[... 565 more characters cut from this display ...]",
 "jobs": "Customer jobs, one per line:\n- Support tickets: accountants copy new invoices into a spreadsheet by hand\n- CSV exports of payments every Friday\n- Do you integrate with our CRM? We want new customers there",
 "evidence": "Users with a live Zap: not given\nLive-run evidence per visible step (step key | live Zap | successful run):\n- new_invoice | no evidence line\n- new_payment | no evidence line\n- invoice_paid_webhook | no evidence line\n- create_customer | no evidence line\n- create_invoice | no evidence line\n- charge_card | no evidence line\n- find_customer | no evidence line\nNo validation error pasted.",
 "carry": "",
 "facts": "The page's checks (4 fail, 5 warn, 8 pass, 2 info, 16 n/a); verdict: not yet - fails to fix first; rule | id | status | evidence:\n- naming | step_names | pass | 7 visible steps, each title-cased with the standard opening\n- naming | field_labels.new_invoice | warn | \"Invoice Number\" - sentence-case field labels (Invoice number), unless a word is a proper noun\n- fields | id_dropdown.create_customer | warn | company_id - an id typed by hand; back it with a dynamic dropdown from a hidden trigger, plus a search for mapped values (D004)\n- fields | delete_copy | na | no delete-like action\n- money | money.charge_card | fail | \"Charge Card\" starts a charge, a transfer or a payment - publishing rules ban facilitating payments or asset transfers; never move money, and confirm anything that starts a charge, such as a payment link, with Zapier partner support\n- auth | auth_test | pass | a test is defined\n- auth | auth_label | pass | a connection label is defined\n- auth | auth_scheme | warn | basic - acceptable but the weakest scheme\n- auth | auth_refresh | na | not OAuth v2\n- auth | auth_401 | na | no refreshing scheme\n- auth | retry_after | warn | no Retry-After on 429 (interview) - let 429 surface with a Retry-After header: Zapier retries it\n- triggers | hook_list | pass | every REST hook (new_payment) has a perform list\n- triggers | hook_subscribe.new_payment | fail | a REST hook while\n\n[... 7908 more characters cut from this display ...]"
}

Reply (shortened for display - see the cuts below):

{
 "task": "plan",
 "summary": "Ledgerwick's next version breaks three live steps, moves money, ships a static webhook and lacks a sample; the plan rejects charge_card, hides the webhook, keeps polling and adds the missing sample before release.",
 "route": "Classify every change first; then Grow; go back to Design for any step whose activation trails",
 "steps": [
  {
   "key": "new_invoice",
   "label": "New Invoice",
   "type": "trigger polling",
   "class": "breaking",
   "reason": "The output key amount becomes total, so later steps mapped to amount in every live Zap get nothing."
  },
  {
   "key": "new_payment",
   "label": "New Payment",
   "type": "trigger hook",
   "class": "breaking",
   "reason": "It switches from polling to a REST hook, a type change that stops its live Zaps."
  },
  {
   "key": "invoice_paid_webhook",
   "label": "Invoice Paid Webhook",
   "type": "trigger hook",
   "class": "hide",
   "reason": "A visible static webhook has no place in a public integration, so it is hidden and replaced."
  },
  {
   "key": "list_customers",
   "label": "List Customers",
   "type": "trigger polling",
   "class": "keep",
   "reason": "The hidden trigger behind the customer dropdown is unchanged."
  },
  {
   "key": "create_customer",
   "label": "Create Customer",
   "type": "create",
   "class": "keep",
   "reason": "Unchanged from the previous version; only its company_id input needs a dropdown."
  }
 ],
 "auth": {
  "decision": "api_key",
  "rests_on": "self-serve API keys: no today - the product issues no OAuth tokens, so a self-serve API key sits on top once keys are self-serve"
 },
 "trigger_types": [
  {
   "key": "new_invoice",
   "decision": "polling",
   "rests_on": "webhook subscription create/delete per Zap: no - the API sorts newest first with stable ids, so polling holds"
  },
  {
   "key": "new_payment",
   "decision": "polling",
   "rests_on": "webhook subscription create/delete per Zap: no - the hook has nothing to subscribe to, so new_payment stays polling"
  },
  {
   "key": "invoice_paid_webhook",
   "decision": "polling",
   "rests_on": "webhook subscription create/delete per Zap: no - a paid-invoice event belongs on a polling trigger"
  }
 ],
 "fixes": [
  {
   "check": "money.charge_card",
   "change": "Remove charge_card from the definition before release; if a payment link is wanted instead, confirm it with Zapier partner support first."
  },
  {
   "check": "hook_subscribe.new_payment",
   "change": "Revert new_payment to the previous version's polling operation, since the API cannot create and delete a webhook subscription per Zap."
  },
  {
   "check": "static_hook.invoice_paid_webhook",
   "change": "Set display.hidden on invoice_paid_webhook and offer a polling New Paid Invoice trigger under a new key in its place."
  },
  {
   "check": "sample_present.find_customer",
   "change": "Add a static sample to find_customer: one realistic customer with id, name and email, keys every account returns."
  },
  {
   "check": "field_labels.new_invoice",
   "change": "Rename the output label Invoice Number to Invoice number."
  }
 ],
 "publish": [
  {
   "item": "Remove charge_card and hide invoice_paid_webhook in the clone, then run validation.",
   "owner": "engineering",
   "week": 1
  },
  {
   "item": "Submit 5-10 Zap templates† for the top jobs on the promoted version: invoices into a spreadsheet, payments into a ledger, new customers into a CRM.",
   "owner": "partnerships",
   "week": 1
  },
  {
   "item": "Embed the first public template in a help article on invoices into a spreadsheet.",
   "owner": "marketing",
   "week": 3
  },
  {
   "item": "Add the Workflow Element to the product's integrations page once a template is public.",
   "owner": "product",
   "week": 3
  }
 ],
 "grow": [
  {
   "lever": "templates",
   "why": "The integration is public, so templates for the three customer jobs are open now and feed every other surface."
  },
  {
   "lever": "launch_embed",
   "why": "No date was given, so the launch-post embed stays second, after the first template is public."
  },
  {
   "lever": "workflow_element",
   "why": "It needs a public template and a domain allowlist, both reachable by week 3."
  },
  {
   "lever": "onboarding",
   "why": "No gate; point the CRM question in support answers at the integration."
  },
  {
   "lever": "spotlight",
   "why": "Silver needs 50 active users† this quarter, and live users are 40."
  },
  {
   "lever": "workflow_api",
   "why": "With 10 engineering days it is possible, but the default order leaves it last."
  }
 ],
 "footnote": "† verify against Zapier's live docs before quoting it.",
 "caveats": [
  "Confirm whether keys can be made self-serve before planning an auth change, which needs a new major."
 ]
}

Cut from this display only (the saved reply and the check use the full text): steps (8 items, 5 shown); fixes (7 items, 5 shown).

What step 5 prints for it: money.charge_card | hook_subscribe.new_payment | static_hook.invoice_paid_webhook | sample_present.find_customer | field_labels.new_invoice | id_dropdown.create_customer | breaking_changes

The page's check: kept - nothing to flag

release - A public integration with breaking changes

The input carries the saved plan (R.laneForm fills carry from the plan reply), as the page's handoff does, and the change diff: 4 changes, 3 breaking, target 2.0.0.

Input (definition is 1423 characters; the first 1396 are shown; diff is 1955 characters; the first 1390 are shown; carry is 2022 characters; the first 1393 are shown; facts is 8943 characters; the first 1398 are shown):

{
 "task": "release",
 "integration": "Integration: Ledgerwick; stage: public; tooling: CLI\nLive users on this version: 40; users with a live Zap: not given\nDate a result must land by: not given; engineering days available: 10\nBreaking changes ship: as pasted - a new major; the old version must stop working (an endpoint sunset): no\nAPI capabilities: webhook subscription create/delete per Zap: no; several subscriptions alive at once: no; newest-first sorting: yes; unique ids that never change format: yes; OAuth issued by the product: no; self-serve API keys: no; returns 401 on expired tokens: yes; Retry-After on 429: no\nRuns its own integrations directory with heavy search traffic: no\nPublish facts: A publicly launched product, not invite-only: yes; You own the API or hold the owner's written permission: yes; Production HTTPS endpoints only: yes; A non-expiring test account with paid features on: yes; An admin whose email domain matches the app's domain: yes; No other public integration for the app: no; The product does not compete with Zapier: no",
 "definition": "The definition as read: version 1.0.3; platformVersion 17.2.0\nAuthentication: basic; fields: none; test: yes; connection label: yes\nSteps (key | label | type | visibility | inputs | outputs | sample | description):\n- new_invoice | New Invoice | trigger polling | visible | inputs 0 | outputs 5 | sample yes | Triggers when a new invoice is created.\n- new_payment | New Payment | trigger hook | visible | inputs 0 | outputs 3 | sample yes | Triggers when a payment is recorded against an invoice.\n- invoice_paid_webhook | Invoice Paid Webhook | trigger hook (static webhook) | visible | inputs 0 | outputs 0 | sample yes | Triggers when an invoice is paid.\n- list_customers | List Customers | trigger polling | hidden | inputs 0 | outputs 0 | sample yes | Triggers when a customer is added. Powers the customer dropdown.\n- create_customer | Create Customer | create | visible | inputs 3 (required: name) | outputs 0 | sample yes | Creates a new customer.\n- create_invoice | Create Invoice | create | visible | inputs 4 (required: customer_id, amount, currency) | outputs 0 | sample yes | Creates a draft invoice for a customer.\n- charge_card | Charge Card | create | visible | inputs 2 (required: customer_id, amount) | outputs 0 | sample yes | Creates a charge on the customer's saved card.\n- find_customer | Find Customer | search | visible | inputs 1 (required: email) | outputs 0 | sample no |\n\n[... 27 more characters cut from this display ...]",
 "previous": "The previous version as read: version 1.0.3; platformVersion 17.2.0\nAuthentication: basic; fields: none; test: yes; connection label: yes\nSteps (key | label | type | visibility | inputs | outputs | sample | description):\n- new_invoice | New Invoice | trigger polling | visible | inputs 0 | outputs 5 | sample yes | Triggers when a new invoice is created.\n- new_payment | New Payment | trigger polling | visible | inputs 0 | outputs 3 | sample yes | Triggers when a payment is recorded against an invoice.\n- invoice_paid_webhook | Invoice Paid Webhook | trigger hook (static webhook) | visible | inputs 0 | outputs 0 | sample yes | Triggers when an invoice is paid.\n- list_customers | List Customers | trigger polling | hidden | inputs 0 | outputs 0 | sample yes | Triggers when a customer is added. Powers the customer dropdown.\n- create_customer | Create Customer | create | visible | inputs 3 (required: name) | outputs 0 | sample yes | Creates a new customer.\n- create_invoice | Create Invoice | create | visible | inputs 3 (required: customer_id, amount) | outputs 0 | sample yes | Creates a draft invoice for a customer.\n- find_customer | Find Customer | search | visible | inputs 1 (required: email) | outputs 0 | sample no | Finds a customer by email.",
 "diff": "The change diff (step | change | class | ship | detail | consequence for live Zaps | the additive alternative):\n- new_invoice | output_renamed | breaking | major | amount -> total | later steps mapped to amount get nothing - promotion does not block it | map the new API shape back to the old output in code, or twin it as new_invoice_v2 with the same name and description, make the change there, hide new_invoice; templates on a hidden step must be rebuilt and resubmitted\n- new_payment | poll_hook_switch | breaking | major | polling -> hook | a type change: live Zaps on new_payment stop (polling -> hook) | twin it as new_payment_v2 with the same name and description, make the change there, hide new_payment; templates on a hidden step must be rebuilt and resubmitted\n- create_invoice | input_required_added | breaking | major | currency | every live Zap lacks a value for currency and errors | give it a default, or twin it as create_invoice_v2 with the same name and description, make the change there, hide create_invoice; templates on a hidden step must be rebuilt and resubmitted\n- charge_card | step_added | additive | keep | create | live Zaps keep running (create) | none needed - additive\nDiff by hand (what the definitions cannot show):\n- The response structure of every step's live results - promotion compares schemas, not live results; it does not block a changed response\n\n[... 565 more characters cut from this display ...]",
 "jobs": "Customer jobs, one per line:\n- Support tickets: accountants copy new invoices into a spreadsheet by hand\n- CSV exports of payments every Friday\n- Do you integrate with our CRM? We want new customers there",
 "evidence": "Users with a live Zap: not given\nLive-run evidence per visible step (step key | live Zap | successful run):\n- new_invoice | no evidence line\n- new_payment | no evidence line\n- invoice_paid_webhook | no evidence line\n- create_customer | no evidence line\n- create_invoice | no evidence line\n- charge_card | no evidence line\n- find_customer | no evidence line\nNo validation error pasted.",
 "carry": "The audit plan's step classes (route: Classify every change first; then Grow; go back to Design for any step whose activation trails):\n- new_invoice: breaking - The output key amount becomes total, so later steps mapped to amount in every live Zap get nothing.\n- new_payment: breaking - It switches from polling to a REST hook, a type change that stops its live Zaps.\n- invoice_paid_webhook: hide - A visible static webhook has no place in a public integration, so it is hidden and replaced.\n- list_customers: keep - The hidden trigger behind the customer dropdown is unchanged.\n- create_customer: keep - Unchanged from the previous version; only its company_id input needs a dropdown.\n- create_invoice: breaking - A required currency input arrives without a default, so every live Zap on it errors.\n- charge_card: rejected - It starts a charge on a saved card, and publishing rules ban moving money.\n- find_customer: keep - Unchanged from the previous version; it still needs a static sample.\nThe plan's fixes:\n- money.charge_card: Remove charge_card from the definition before release; if a payment link is wanted instead, confirm it with Zapier partner support first.\n- hook_subscribe.new_payment: Revert new_payment to the previous version's polling operation, since the API cannot create and delete a webhook subscription per Zap.\n- static_hook.invoice_paid_webhook: Set display.hidden on\n\n[... 629 more characters cut from this display ...]",
 "facts": "The page's checks (4 fail, 5 warn, 8 pass, 2 info, 16 n/a); verdict: not yet - fails to fix first; rule | id | status | evidence:\n- naming | step_names | pass | 7 visible steps, each title-cased with the standard opening\n- naming | field_labels.new_invoice | warn | \"Invoice Number\" - sentence-case field labels (Invoice number), unless a word is a proper noun\n- fields | id_dropdown.create_customer | warn | company_id - an id typed by hand; back it with a dynamic dropdown from a hidden trigger, plus a search for mapped values (D004)\n- fields | delete_copy | na | no delete-like action\n- money | money.charge_card | fail | \"Charge Card\" starts a charge, a transfer or a payment - publishing rules ban facilitating payments or asset transfers; never move money, and confirm anything that starts a charge, such as a payment link, with Zapier partner support\n- auth | auth_test | pass | a test is defined\n- auth | auth_label | pass | a connection label is defined\n- auth | auth_scheme | warn | basic - acceptable but the weakest scheme\n- auth | auth_refresh | na | not OAuth v2\n- auth | auth_401 | na | no refreshing scheme\n- auth | retry_after | warn | no Retry-After on 429 (interview) - let 429 surface with a Retry-After header: Zapier retries it\n- triggers | hook_list | pass | every REST hook (new_payment) has a perform list\n- triggers | hook_subscribe.new_payment | fail | a REST hook while\n\n[... 7545 more characters cut from this display ...]"
}

Reply:

{
 "task": "release",
 "summary": "Three breaking changes ship in place, so Ledgerwick moves to 2.0.0 and every user updates their Zaps by hand; charge_card stays out of the release because it moves money.",
 "target": {
  "version": "2.0.0",
  "why": "Breaking changes shipped as pasted need a new major, and Zapier blocks user migration across majors; as twins they would stay in 1.1.0."
 },
 "changes": [
  {
   "step": "new_invoice",
   "change": "output_renamed",
   "class": "breaking",
   "ship": "major",
   "note": "Map total back to amount in the perform code, or keep the rename and tell users to remap amount in 2.0.0."
  },
  {
   "step": "new_payment",
   "change": "poll_hook_switch",
   "class": "breaking",
   "ship": "major",
   "note": "The API cannot back a REST hook, so keep new_payment polling even in 2.0.0."
  },
  {
   "step": "create_invoice",
   "change": "input_required_added",
   "class": "breaking",
   "ship": "major",
   "note": "Give currency a default so Zaps updated to 2.0.0 keep running without a new mapping."
  },
  {
   "step": "charge_card",
   "change": "step_added",
   "class": "additive",
   "ship": "keep",
   "note": "Additive in the diff, but the audit rejects it: remove it before promoting, since publishing rules ban moving money."
  }
 ],
 "by_hand": [
  {
   "what": "The response structure of every step's live results",
   "how": "Run each step in the clone against a test account and compare its live result with the previous version's, key by key."
  },
  {
   "what": "The sort order, page contents and id format of new_invoice",
   "how": "Confirm the clone still returns invoices newest first, the same page size and the same integer ids as 1.0.3."
  },
  {
   "what": "The perform behavior of new_payment",
   "how": "Test the clone's new_payment against live Zaps on a test account before promoting."
  }
 ],
 "canary": {
  "how": "A canary of 5-10%† of traffic for 30-60 minutes† covers non-breaking changes only, so run it on the 1.0.x clone without the breaking changes, then share 2.0.0 with test accounts.",
  "watch": "Watch Monitoring for new errors on new_invoice and create_invoice before asking users to move."
 },
 "migrate": {
  "when": "Promote 2.0.0, then ask every user to update each Zap by hand - automatic migration stops at the major boundary.",
  "deprecate": "mark legacy - 1.0.3 keeps working, so nothing has to stop on a date."
 },
 "templates": [],
 "notice": "Ledgerwick 2.0.0 is available in Zapier.\nNew Invoice now outputs Total under a new field name: remap it in each Zap that uses it.\nCreate Invoice asks for a currency.\nOpen each Zap and switch it to 2.0.0 - Zaps on 1.0.3 keep running until you do.",
 "footnote": "† verify against Zapier's live docs before quoting it.",
 "caveats": [
  "Confirm with the team whether the twin route to 1.1.0 is cheaper than asking 40 live users to update by hand."
 ]
}

What step 5 prints for it: new_invoice output_renamed -> major | new_payment poll_hook_switch -> major | create_invoice input_required_added -> major | charge_card step_added -> keep

The page's check: kept - nothing to flag

release - A clean additive release

The input carries the saved plan (R.laneForm fills carry from the plan reply), as the page's handoff does, and the change diff: 4 changes, 0 breaking, target 1.1.0.

Input (facts is 7187 characters; the first 1396 are shown):

{
 "task": "release",
 "integration": "Integration: Tasquill; stage: public; tooling: CLI\nLive users on this version: 120; users with a live Zap: not given\nDate a result must land by: not given; engineering days available: 15\nBreaking changes ship: as twins - stay in this major; the old version must stop working (an endpoint sunset): no\nAPI capabilities: webhook subscription create/delete per Zap: no; several subscriptions alive at once: no; newest-first sorting: yes; unique ids that never change format: yes; OAuth issued by the product: yes; self-serve API keys: yes; returns 401 on expired tokens: yes; Retry-After on 429: yes\nRuns its own integrations directory with heavy search traffic: no\nPublish facts: A publicly launched product, not invite-only: yes; You own the API or hold the owner's written permission: yes; Production HTTPS endpoints only: yes; A non-expiring test account with paid features on: yes; An admin whose email domain matches the app's domain: yes; No other public integration for the app: no; The product does not compete with Zapier: no",
 "definition": "The definition as read: version 1.0.6; platformVersion 17.4.0\nAuthentication: oauth2; fields: none; test: yes; connection label: yes; autoRefresh: yes\nSteps (key | label | type | visibility | inputs | outputs | sample | description):\n- new_task | New Task | trigger polling | hidden | inputs 0 | outputs 3 | sample yes | Triggers when a new task is added to a project.\n- new_project | New Project | trigger polling | visible | inputs 0 | outputs 2 | sample yes | Triggers when a new project is created.\n- new_task_v2 | New Task | trigger polling | visible | inputs 0 | outputs 3 | sample yes | Triggers when a new task is added to a project.\n- create_task | Create Task | create | visible | inputs 4 (required: project_id, title) | outputs 0 | sample yes | Creates a task in a project.\n- find_project | Find Project | search | visible | inputs 1 (required: name) | outputs 0 | sample yes | Finds a project by name.\n- find_task | Find Task | search | visible | inputs 2 (required: title) | outputs 0 | sample yes | Finds a task by title in a project.",
 "previous": "The previous version as read: version 1.0.6; platformVersion 17.4.0\nAuthentication: oauth2; fields: none; test: yes; connection label: yes; autoRefresh: yes\nSteps (key | label | type | visibility | inputs | outputs | sample | description):\n- new_task | New Task | trigger polling | visible | inputs 0 | outputs 3 | sample yes | Triggers when a new task is added to a project.\n- new_project | New Project | trigger polling | visible | inputs 0 | outputs 2 | sample yes | Triggers when a new project is created.\n- create_task | Create Task | create | visible | inputs 3 (required: project_id, title) | outputs 0 | sample yes | Creates a task in a project.\n- find_project | Find Project | search | visible | inputs 1 (required: name) | outputs 0 | sample yes | Finds a project by name.",
 "diff": "The change diff (step | change | class | ship | detail | consequence for live Zaps | the additive alternative):\n- new_task | step_hidden | hide | hide | display.hidden true | live Zaps on new_task keep running; new Zaps cannot pick it; Zap templates built on it turn invalid | rebuild and resubmit the Zap templates built on new_task on its replacement\n- new_task_v2 | step_twin | additive | keep | twins new_task | live Zaps keep running (twins new_task) | none needed - additive\n- create_task | input_added_optional | additive | keep | due_on | live Zaps keep running (due_on) | none needed - additive\n- find_task | step_added | additive | keep | search | live Zaps keep running (search) | none needed - additive\nDiff by hand (what the definitions cannot show):\n- The response structure of every step's live results - promotion compares schemas, not live results; it does not block a changed response structure\n- The sort order, page contents and id format of new_project - a change floods every live Zap on the next poll - the dedupe memory holds the old ids\n- The perform behavior of create_task - its perform changed between the versions; promotion does not block changed perform behavior - test it against live Zaps first\nZap templates to rebuild: Add new Tasquill tasks to a shared calendar (on new_task)\nTarget version: 1.1.0 - additive changes only - 1.1.0, the next minor of the same major",
 "jobs": "Customer jobs, one per line:\n- New tasks onto a shared calendar\n- Create tasks from new form entries\n- Find a project before adding a task to it",
 "evidence": "Users with a live Zap: not given\nLive-run evidence per visible step (step key | live Zap | successful run):\n- new_project | no evidence line\n- new_task_v2 | no evidence line\n- create_task | no evidence line\n- find_project | no evidence line\n- find_task | no evidence line\nNo validation error pasted.",
 "carry": "The audit plan's step classes (route: Classify every change first; then Grow; go back to Design for any step whose activation trails):\n- new_task: hide - Hidden behind its twin, so its live Zaps keep running while new Zaps use new_task_v2.\n- new_project: keep - Unchanged; it also feeds the project dropdown.\n- new_task_v2: additive - The twin of new_task under a new key, with the same name and description and string ids.\n- create_task: additive - It gains an optional due date, which no live Zap has to fill.\n- find_project: keep - Unchanged; it backs mapped project values.\n- find_task: additive - A new search under a new key.\nThe plan's fixes:\n- sample_shape.new_project: Drop custom_color from the new_project sample so it carries only keys every account returns.",
 "facts": "The page's checks (0 fail, 1 warn, 15 pass, 2 info, 17 n/a); verdict: passes with warnings to settle; rule | id | status | evidence:\n- naming | step_names | pass | 5 visible steps, each title-cased with the standard opening\n- naming | field_labels | pass | every field label sentence-cased\n- fields | id_dropdown | pass | every id input on create_task, find_task has a dynamic dropdown\n- fields | delete_copy | na | no delete-like action\n- money | money | pass | 1 action, none starting a charge, a transfer or a payment link\n- auth | auth_test | pass | a test is defined\n- auth | auth_label | pass | a connection label is defined\n- auth | auth_scheme | pass | oauth2 - the preferred scheme\n- auth | auth_refresh | pass | autoRefresh is on\n- auth | auth_401 | pass | returns 401 on expired tokens (interview)\n- auth | retry_after | pass | Retry-After on 429 (interview)\n- triggers | hook_list | na | no REST hook trigger\n- triggers | hook_subscribe | na | no REST hook trigger\n- triggers | static_hook | na | no static webhook\n- triggers | poll_contract | pass | new_project, new_task_v2 - newest-first sorting and stable unique ids (interview); Zapier never reads page two, so a page must cover a burst\n- triggers | updated_id | na | no updated trigger\n- samples | sample_present | pass | 5 visible steps, each with a static sample\n- samples | sample_shape.new_project | warn | custom_color look\n\n[... 5791 more characters cut from this display ...]"
}

Reply:

{
 "task": "release",
 "summary": "Every change is additive or a hidden step, so Tasquill ships 1.1.0 inside the same major, migrates users automatically and rebuilds the one Zap template built on new_task.",
 "target": {
  "version": "1.1.0",
  "why": "No breaking change: 1.0.6 is public, so the clone goes to the next minor of the same major."
 },
 "changes": [
  {
   "step": "new_task",
   "change": "step_hidden",
   "class": "hide",
   "ship": "hide",
   "note": "Keep new_task working for its live Zaps and rebuild the template built on it against new_task_v2."
  },
  {
   "step": "new_task_v2",
   "change": "step_twin",
   "class": "additive",
   "ship": "keep",
   "note": "Check the twin keeps the same name and description as new_task."
  },
  {
   "step": "create_task",
   "change": "input_added_optional",
   "class": "additive",
   "ship": "keep",
   "note": "The optional due date needs no value from existing Zaps."
  },
  {
   "step": "find_task",
   "change": "step_added",
   "class": "additive",
   "ship": "keep",
   "note": "Add a help-article line for the new search."
  }
 ],
 "by_hand": [
  {
   "what": "The response structure of every step's live results",
   "how": "Compare each step's live result in the clone with 1.0.6 on a test account, key by key."
  },
  {
   "what": "The sort order, page contents and id format of new_project",
   "how": "Confirm new_project still returns projects newest first with the same integer ids."
  },
  {
   "what": "The perform behavior of create_task",
   "how": "Run create_task with and without a due date against a test project before promoting."
  }
 ],
 "canary": {
  "how": "Canary 1.1.0 at 5-10%† of traffic for 30-60 minutes† with the CLI; every change here is non-breaking.",
  "watch": "Watch Monitoring for new errors on create_task and new_task_v2 before migrating the rest."
 },
 "migrate": {
  "when": "Promote 1.1.0, migrate 5% of users first, then the rest - all inside the 1.x major.",
  "deprecate": "mark legacy - 1.0.6 does not have to stop working."
 },
 "templates": [
  "Add new Tasquill tasks to a shared calendar (on new_task)"
 ],
 "notice": "Tasquill 1.1.0 is live in Zapier.\nCreate Task can now set a due date.\nA new Find Task search looks up a task by title.\nZaps you already run keep working; nothing to do.",
 "footnote": "† verify against Zapier's live docs before quoting it.",
 "caveats": []
}

What step 5 prints for it: new_task step_hidden -> hide | new_task_v2 step_twin -> keep | create_task input_added_optional -> keep | find_task step_added -> keep

The page's check: kept - nothing to flag

Build the input yourself

Load the engine in node (global.window = global; require("./zpkit.js"); require("./zpreply.js")), then:

const K = ZpKit, R = ZpReply;
const prev = { version: "1.0.0", authentication: { type: "basic", test: { url: "https://api.acme.example/me" } },
  triggers: { new_invoice: { key: "new_invoice", noun: "Invoice", display: { label: "New Invoice", description: "Triggers when a new invoice is created." },
    operation: { type: "polling", sample: { id: 1, amount: 9 }, outputFields: [{ key: "id", label: "Invoice ID" }, { key: "amount", label: "Total" }] } } } };
const next = JSON.parse(JSON.stringify(prev)); next.triggers.new_invoice.operation.outputFields[1].key = "total";
const f = K.form(Object.assign(K.blank(), { stage: "public", tooling: "cli", definition: JSON.stringify(next), previous: JSON.stringify(prev) }));
// f.parsed and f.prevParsed (an error with its line and column), f.def and f.prev (the definitions as read),
// f.rows (the checks), f.classes (each step class), f.changes (rows, byHand, breaking), f.target (the next version),
// f.auth, f.triggers, f.grow, f.timeline (impossible goals first), f.templateList, f.route_text
// K.stepsCsv(f), K.checksCsv(f), K.diffCsv(f), K.board(f), K.applyPlan(f, releaseReply), K.releaseNotes(f) - the exports
const b = K.buildInput(f, "release");                        // or "plan" (R.laneForm(form, "release", planReply) fills carry)
const wire = K.toWire(b.input);                              // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "release");
// R.check(reply, b)                                         // {status: kept or flagged, notes, fixed}

Get a token for the steps below from /tokens.html.

Step 1 — a tiny client helper

One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN"   # from https://zapier-release.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"zapier-release"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "plan", "integration": "Integration: Ledgerwick; stage: public; tooling: CLI
...", "definition": "The definition as read: version 1.0.3; platformVersion 17.2.0
...", "previous": "The previous version as read: version 1.0.3; platformVersion 17.2.0
...", "diff": "The change diff (step | change | class | ship | detail | consequence for live Zaps | the additive alternative):
...", "jobs": "Customer jobs, one per line:
...", "evidence": "Users with a live Zap: not given
...", "carry": "", "facts": "The page's checks (...); verdict: not yet - fails to fix first; rule | id | status | evidence:
- naming | step_names | pass | 7 visible ..."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses zapier-release:<task>:<inputHash>:a<attempt> - the lane is part of the key, retry_note is left out of the hash - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A plan reply carries summary, route, steps, auth, trigger_types, fixes, publish, grow, footnote and caveats; a release-lane reply carries summary, target, changes, by_hand, canary, migrate, templates, notice, footnote and caveats. The code below prints the gist of either: each fix of the plan, or each change with how it ships.

KEY="zapier-release:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r 'if .task == "release" then (.changes[] | "\(.step) \(.change) -> \(.ship)") 
       else (.fixes[] | "\(.check) \(.change)") end' reply.json

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: zapier-release:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json