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.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The 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. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: both lanes are metered and need a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. 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:
plan- the audit and publish plan: the page's route, every definition step with the page's type and class (keep, additive, hide, breaking or rejected), the auth pick and each trigger's type with the capability it rests on, one fix per failing check, the publish checklist with an owner role and a week from the page's timeline, and the grow levers in the page's order - every Zapier platform number with its dagger.release- the release planned: the page's target version, one row per change with the page's class and ship (twin, hide, major or keep - a breaking change never ships as keep), what to diff by hand, the canary for the tooling, the migration and legacy or a dated deprecation, the Zap templates to rebuild, and a short notice to users - one item per line, never promising Zapier will email everyone. Needs the previous version inprevious.
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
| Field | Required | What |
|---|---|---|
task | yes | plan or release. If it is missing or neither, do plan and set "task": "plan". |
integration | yes | the 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. |
definition | yes | the 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. |
previous | send it (empty with no previous version; release needs it) | the previous version read the same way, or empty. |
diff | send 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. |
jobs | yes (says so when none were given) | the customer jobs, one per line. |
evidence | send it | users with a live Zap, the live-run evidence per visible step and the validation lines pasted. |
carry | send it (empty for plan) | release task only: the audit plan's step classes and fixes when the plan ran first; may be empty. |
facts | yes | the 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_note | no | optional; 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:
- Scope: a vendor's own Zapier integration - building it, publishing it, growing it and changing it without breaking live Zaps. Not building Zaps as an end user, not calling existing Zapier apps from code or agents, not choosing an embedded iPaaS, not generic marketplace listing work.
- Breaking changes for live Zaps: renaming or removing a trigger, action, input-field or output-field key; changing an output's response structure or an input field's type; switching a trigger between polling and REST hook; changing the auth scheme, renaming an auth field or adding a required auth field; adding a required input field unless it defaults; changing what counts as new on a live polling trigger (sort order, page contents or id format) - each live Zap's dedupe memory holds the old ids, so the first poll fires every unseen item as new.
- Ship the replacement beside the original: copy the step under a new key (new_invoice_v2) with the same name and description, make the change there, hide the old step. Old Zaps keep running; new Zaps get the new step. Hiding a step invalidates every Zap template built on it: rebuild and resubmit those templates on the new key. Breaking changes that cannot ship this way need a new major, and Zapier blocks user migration across majors: every user updates their Zaps manually.
- Promotion blocks most schema-level breaking changes inside a major, but not renamed or removed output keys, changed response structure or changed perform behavior. Diff those by hand before every promotion.
- Zapier gives you no user email list; you reach users only through your own channels.
- Design: pick steps from customer jobs, not from the API reference. Order: event triggers on core objects, then creates, then searches that feed dropdowns and search-or-create, then updates, then everything else. Ship four to eight good steps before twenty thin ones. Avoid deletes, and give any delete a copy field saying it is irreversible. Step names title-cased (New Invoice, Create Customer, Find Invoice), field labels sentence-cased, descriptions "Triggers when...", "Creates a...", "Finds a...".
- Auth: OAuth v2 when the product already issues OAuth tokens, else a self-serve API key. Basic auth is acceptable but weakest. Users who must email support for a key block publication. Return HTTP 401 on expired tokens: Zapier refreshes on 401 only.
- Trigger type: a REST hook (shown as Instant) when the API can create and delete webhook subscriptions per Zap and keep several alive at once; otherwise polling. Never a static webhook in a public integration. Every hook trigger needs a perform list returning the exact payload schema.
- Polling: newest first, a unique primary key that never changes format, enough items per page to cover a burst (Zapier never reads page two). An updated trigger needs a composite id such as id-updated_at, with the original id kept in another field; prefer a specific state trigger. A trigger that routinely exceeds 100 items† per poll belongs on a hook.
- Fields: no input a user must type an id into - back each with a dynamic dropdown from a hidden trigger, plus a search for mapped values. Output: id, human name, a link to the record, ISO 8601 dates with offset. Static samples: one item, realistic values, only keys every account returns.
- Errors: never 200 on a failure; a user-fixable error names the field and the reason in under 250 characters†; a halted error for unmet preconditions; let 429 surface with a Retry-After header.
- Never move money: publishing rules ban facilitating payments or asset transfers. Recording a completed payment is fine; confirm anything that starts a charge, such as a payment link, with Zapier partner support before building it.
- A version is editable only while private with fewer than 5 users†; otherwise clone it to the next minor of the same major (1.0.3 to 1.1.0), fix the clone, promote, migrate. Prefer additive fixes inside the current major.
- Publish: at least 3 users† with a live Zap, one live Zap and one successful run per visible step in the admins' Zap history; hide steps nobody can exercise. Clear every validation Error and Publishing Task. Eligibility: a publicly launched product, API ownership, production HTTPS endpoints, a non-expiring test account with paid features, an admin with a company-domain email, one public integration per app, not a Zapier competitor. A reviewer replies within about a week†; each fix round adds about another week†. Approval lists the integration as Beta for 90 days† or until the next business day after one embed-sourced signup. Count backwards from a date: recruiting comes before submission, templates exist only after approval, template review takes about 2 weeks†, and four weeks† between submission and the date is the happy-path minimum.
- Grow, in order of efficiency: Zap templates, a template embedded in the launch post and help articles, the Workflow Element on the product's integrations page, Zapier in onboarding, support answers and the empty integration search, Zapier spotlight benefits (Silver and up, 50 active users†), a custom marketplace on the Workflow API. A date makes the first public template, embedded in the launch post, the fastest route.
- Maintain: a Zap erroring on 95%† of more than 20 runs† over 7 days† turns off. Zapier documents no integration-level error threshold for delisting; never promise one. For any change: clone, change, validate and diff by hand before promoting. CLI: a canary of 5-10%† of traffic for 30-60 minutes†, non-breaking changes only. Platform UI: share the clone with test accounts. Then promote, watch Monitoring, migrate the rest. Deprecate only a version that will stop working; otherwise mark it legacy. A deprecation date sits 3 weeks† to 1 year† ahead; Zapier emails affected users 14 days† before.
- Every Zapier platform number carries † right after it. Close with the footnote: "† verify against Zapier's live docs before quoting it."
House rules
- Use only the input. Never invent a step, a key, a field, a version, a user count, a date, a template or a URL; the only figures you may quote are the skill's, each with † as the facts mark it, and the input's own values. When the input does not say, write "not stated in the input".
- The page's verdicts are settled: every check status, the route, every step's type and class, the auth pick, every trigger decision, the lever order, every change's class and ship, and the target version. Never move them; explain them.
- Every Zapier platform number carries † right after it; the
footnotefield is exactly "† verify against Zapier's live docs before quoting it." - Never imply that Zapier endorses, recommends, vets or certifies the integration.
- Never promise an integration-level error threshold for delisting: Zapier documents none.
- No URL: never write a scheme such as https and never copy a link. Write "the help article", "the launch post" or "the integration's page" where a link belongs.
- Write in the language of the input; keep the JSON keys, step keys, check ids and enum values exactly as given.
- Never use backticks, never write the input's field names as labels in your text, and never copy this prompt's wording into a reply.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
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.
summary- one sentence, at most 40 words: what stands between the integration and a clean publish or release today, and what the plan changes.route- exactly the facts' route.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,searchorsearchOrCreate),class(exactly the facts' class:keep,additive,hide,breakingorrejected),reason(one sentence).auth- an object:decision(exactly the facts' pick:oauth2,api_key,session,basicordigest),rests_on(the capability the facts name, quoted exactly, and what the input says about it).trigger_types- one object per visible trigger:key,decision(exactly the facts' decision:pollingorhook),rests_on(quote "webhook subscription create/delete per Zap" and what the input says about it).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.publish- the publish checklist, one object per item:item(one sentence),owner(a role, one ofengineering,product,partnerships,support,marketing,integration admin- never a person),week(a week number from the facts' timeline).grow- one object per lever, in the facts' order:lever(exactly the facts' lever id:templates,launch_embed,workflow_element,onboarding,spotlightorworkflow_api),why(one sentence naming the answer that moved it, or why it sits where it does).footnote- exactly the footnote.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.
summary- one sentence, at most 40 words: what changes for live Zaps and how the release keeps them running.target- an object:version(exactly the facts' target version),why(one sentence: the version rule behind it).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,hideorbreaking),ship(exactly the row's ship:twin,hide,majororkeep- a breaking change never ships as keep),note(one sentence: what the team does for it).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).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).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).templates- the facts' list of Zap templates to rebuild, each string exactly as given;[]when the list is empty.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.footnote- exactly the footnote.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"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://zapier-release.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// zapier-release.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://zapier-release.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "YOUR_TOKEN" // from https://zapier-release.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// ZapierRelease.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class ZapierRelease {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://zapier-release.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://zapier-release.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "YOUR_TOKEN"; // from https://zapier-release.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "YOUR_TOKEN"; // from https://zapier-release.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
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')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "zapier-release"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "zapier-release" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "zapier-release"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "zapier-release"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "zapier-release" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "zapier-release"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "zapier-release" }))!["token"]!.GetValue<string>();
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}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["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}'
inp = json.load(open("input.json")) # {"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
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "plan" or "release", integration, definition, previous, diff, jobs, evidence, carry, facts} (see the input table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
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
import hashlib, time
key = "zapier-release:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply["task"])
if reply["task"] == "release":
for x in reply["changes"]:
print(x["step"], x["change"], "->", x["ship"])
print(reply["target"]["version"], reply["migrate"]["deprecate"])
else:
for x in reply["fixes"]:
print(x["check"], x["change"])
import { createHash } from "node:crypto";
const key = `zapier-release:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.task);
if (reply.task === "release") { for (const x of reply.changes) console.log(x.step, x.change, "->", x.ship); console.log(reply.target.version, reply.migrate.deprecate); }
else { for (const x of reply.fixes) console.log(x.check, x.change); }
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "zapier-release:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
fmt.Println(j["charged_credits"], j["truncated"], reply["task"])
key, name, field := "fixes", "check", "change"
if reply["task"] == "release" {
key, name, field = "changes", "change", "ship"
}
ls, _ := reply[key].([]any)
for _, x := range ls {
xx := x.(map[string]any)
fmt.Println(xx[name], xx[field])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "zapier-release:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply.path("task").asText());
if (reply.path("task").asText().equals("release")) {
for (JsonNode x : reply.path("changes")) System.out.println(x.path("change").asText() + " -> " + x.path("ship").asText());
} else {
for (JsonNode x : reply.path("fixes")) System.out.println(x.path("check").asText() + " " + x.path("change").asText());
}
require "digest"
key = "zapier-release:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["task"]].inspect
if reply["task"] == "release"
reply["changes"].each { |x| puts "#{x["change"]} -> #{x["ship"]}" }
else
reply["fixes"].each { |x| puts "#{x["check"]} #{x["change"]}" }
end
$key = "zapier-release:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["task"], "\n";
if ($reply["task"] === "release") { foreach ($reply["changes"] as $x) echo $x["change"], " -> ", $x["ship"], "\n"; }
else { foreach ($reply["fixes"] as $x) echo $x["check"], " ", $x["change"], "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"zapier-release:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>())!;
var task = (string?)reply["task"];
Console.WriteLine($"{j["charged_credits"]} {task}");
if (task == "release") { foreach (var x in reply["changes"]!.AsArray()) Console.WriteLine($"{x!["change"]} -> {x["ship"]}"); }
else { foreach (var x in reply["fixes"]!.AsArray()) Console.WriteLine($"{x!["check"]} {x["change"]}"); }
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
rev = json.load(open("input.json")) # the same run input as input.json: {"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
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "zapier-release:" + rev["task"] + ":" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `zapier-release:${rev.task}:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "zapier-release:"+input["task"].(string)+":"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "zapier-release:" + input.get("task") + ":" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "zapier-release:#{JSON.parse(rev)["task"]}:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: zapier-release:" . json_decode($rev, true)["task"] . ":" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"zapier-release:{input["task"]}:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);