How to build a Zapier alternative
How to build a Zapier alternative as one signed webhook, a field map, and a single HTTP action that runs once per event. Prompts and checks inside.
How to build a Zapier alternative is one signed webhook, a short list of steps, and one HTTP call that runs once. Zapier is that product. The catalog line is “You need a few reliable workflows, not 7,000 connectors.”
You pay $30–$69 a month. The first estimate is 3 days, 24 hours. The verdict is “For your five automations”.
The Zapier post is the teardown. It covers the queue, replay, and the connector directory. This guide builds the 3-day row, with the screens and the prompts in order.
A scheduled fetch between three internal systems is Make, at $10–$29 a month, about 4 days. Five personal applets are IFTTT, at $2.99–$14.99 a month, about 8 hours, and the IFTTT post is that split. Whenhook is the company webhook.
How to build a Zapier alternative as one hook and one POST
The Zapier page splits the work into three rows.
The first row is 3 days. A webhook trigger, a transform step, and an HTTP action. This guide keeps that row and adds the filter the teardown already draws on the 3-day half, because a paid-plan check is the reason the POST exists. The catalog names the project Whenhook.
The second row is 2 weeks and up, 80 hours. Linear workflows, run history, retries, secrets, and replay. This v1 stores one run so you can see the POST. Search, backoff, and a replay button stay on that later row. The third row is 3 months and up, 480 hours. The catalog writes that as a 7,000-connector marketplace. The teardown notes that Zapier’s help center now says 9,000+ apps.
If you tell an agent to “clone Zapier”, it aims at the third row. Paste the prompts below. One workflow. One published revision. One destination.
The example is New paid lead. The hook is /hooks/new-lead. Ada’s event is evt-8841, with plan pro and email [email protected]. Lea’s event has plan starter. The POST goes to https://crm.flaviocopes.com/leads.
Where the hook and the POST split
Every product prompt ships in two flavors: Node.js plus Astro, and TanStack Start. The card lists Workers, Workflows, and D1. This site is an Astro app on Cloudflare, so the pages are Astro and the runner is a Worker queue. Use TanStack Start if React is your weekday language. The stack post is which tab you copy. The prompt post is how you pin the first estimate.
The browser shows an editor, a sample console, and a run page. Astro on the request saves the draft, freezes a revision on Publish, and accepts the webhook. It checks the signature, stores the event, and returns 200 before any step runs. D1 holds the workflow, the revision, the steps, the runs, and each step output. A queue consumer runs one step, writes that output, then starts the next. The CRM is the only system outside your app.
TanStack Start keeps these screens, with server functions, a SQL database, and a job runner in place of the queue. The hook still returns before the POST.
What one run carries
The card lists Workflow, Step, SecretReference, Run, and StepRun. This v1 uses all five. A delay step and a second destination wait.
A workflow has a name, an enabled flag, and a pointer to the published revision. New paid lead starts disabled. Enable is allowed only after a revision exists. Disable stops new runs and lets one that already started finish.
A revision stores the hook path, the secret reference, and the ordered steps. Publish writes revision 1. Later edits stay on a draft until you publish revision 2. A run keeps the revision it started with.
A step is filter, transform, or http. Filter-paid passes only when trigger.body.plan is pro. Map-email copies trigger.body.email. Post-crm sends that email to the CRM. A missing path fails the run and names the path. It does not send the text undefined.
A secret reference is a name, new-lead-hook, stored apart from the steps. The run page shows the name and never the value. A run has the event key, the revision id, and a status of running, filtered, succeeded, or failed. Lea’s rejection is filtered, and the HTTP step is skipped. A step run stores the output before the next step starts.
const run = {
eventKey: 'evt-8841',
revision: 1,
}
A second delivery with that key returns 200 and does not insert another run.
The screens you ship
Three surfaces, all signed in. The webhook URL is the only public route, and it accepts a POST rather than rendering a page.
The editor is /workflows/new-paid-lead. It shows the name, enabled or disabled, and the published revision. Under that, the three steps in order. The hook path and the secret name sit above the steps. Save writes the draft. Publish stays disabled until every step has a complete config and every path points at the trigger or an earlier step.
The sample console shows the hook URL, whether the secret is set, and the last captured body. Send sample runs Ada’s payload through the same path as a real hook, and it does not call the CRM before publish. A rejected signature shows a reason category and none of the body.
The run page is /runs/evt-8841. It shows the status, the revision number, and the three steps with a short redacted output. Ada’s map step shows [email protected]. The POST step shows the CRM status code. The secret value is absent. Lea’s run shows Filtered on the first step and Skipped on the POST.
The path from a sample to one POST
Publish revision 1, send Ada’s signed event, and send it again. One run, one POST. Lea’s starter plan is filtered, and a bad signature adds nothing.
Prompt 1. A draft and a captured sample
Build Whenhook, one webhook
workflow.
Use Node.js and Astro for
the pages. Ordinary HTML
forms. Cloudflare D1 for
the rows. A queue consumer
runs steps outside the
request. If you use
TanStack Start instead,
use server functions, a
SQL database, and a job
runner. Same screens.
The owner is the only
person who can open the
editor. Seed one disabled
workflow named New paid
lead. Hook path
/hooks/new-lead. Secret
reference new-lead-hook.
The value is stored apart
from the step config and
is never rendered.
Steps, in order:
filter-paid passes only
when trigger.body.plan
is pro. map-email copies
trigger.body.email.
post-crm POSTs that email
to https://crm.flaviocopes.com/leads
and sends the run id with
the step id as an
idempotency key.
The sample console shows
the URL, whether the
secret is set, and the
last captured body. Send
sample uses Ada's body:
plan pro, email
[email protected]. It does not
call the CRM until a
revision is published.
No canvas. No app
directory. No polling.
No second workflow.
Done when the three steps
reload in order and the
secret value is absent
from the HTML.
What to check after the draft
Reload the editor. The steps are filter, map, and POST, and the secret field shows the name new-lead-hook only.
Send the sample before publish. You get a message that nothing is published, and the run list is empty.
Prompt 2. Publish a revision
Add Publish.
Publishing is allowed only
when the secret is set,
the URL is https, and
every path points at the
trigger or an earlier
step. A path that points
forward is an error that
names the step.
Publish freezes revision
1: path, secret reference,
and the three step configs.
The draft can change after
that. The frozen revision
cannot. The hook runs the
published revision only.
Enable and disable sit on
the workflow. Disable
stops new runs and lets
an in-flight run finish
on the revision it pinned.
A missing path at publish
time blocks the button.
Do not coerce a missing
value into the word
undefined.
Done when Publish creates
revision 1, the editor
still has a separate
draft, and a forward
reference cannot publish.
What to check after publish
Publish, then change the CRM URL on the draft and save. Revision 1 still has the original URL. The editor shows the new URL as unpublished.
Point map-email at steps.post-crm.response.id and try to publish. The error names map-email. Revision 1 is unchanged.
Prompt 3. Accept the hook and run the steps
Add the public POST at
/hooks/new-lead.
Read the raw body. Verify
the HMAC signature with
the secret, using a
constant-time compare.
Reject a timestamp older
than the window you show
in the console. Cap the
body size before you
store it.
On a bad signature, return
an error, store no run,
and log only the reason
category. On success,
store one run for the
provider event id. If the
body has no id, derive
the key from the signed
body. A repeat key returns
200 and does not enqueue
a second run.
Return 200 as soon as the
run row exists. The queue
runs filter, then
transform, then HTTP.
Write each step output
before starting the next
step. A filter miss sets
the run to filtered and
skips the POST. A missing
email path sets the run
to failed and names the
path. The HTTP call uses
the same idempotency key
if the worker retries it.
Done when Ada's event
posts once, Lea's starter
event does not post, and
a second evt-8841 adds
no run.
What to check after a real event
Send Ada’s signed body. The HTTP status is 200 before the CRM responds. The run page later shows succeeded, [email protected], and one CRM status.
Send it again with the same id. There is still one run. Send Lea’s body with plan starter and a new id. That run is filtered, and the CRM log shows no second lead.
Flip one byte of the signature. The response is rejected, the run count does not change, and the log line has no email address in it.
Prompt 4. A run page that hides the secret
Add /runs/:eventKey for
the owner.
Show workflow name,
revision, status, and the
steps in order. Each step
shows status and a short
output. Redact the secret
value and any header that
carries it. A filtered run
shows skipped on later
steps, not failed.
The list on the workflow
shows the latest runs
with status and time.
There is no search across
a history archive, no
replay button, and no
backoff schedule.
A signed-out request is
denied, and the body does
not include outputs.
Done when evt-8841 shows
the email and the CRM
status, and the secret
string appears nowhere
on the page.
What to check after the run page
Open Ada’s run. You see revision 1, the email, and the CRM status. Search the HTML for the secret value and find nothing.
Open Lea’s run. The filter says filtered and the POST says skipped. The workflow status is not failed.
Sign out and request the run URL. You get the login wall, and the body has no email.
Failures the agent ships anyway
The POST happens inside the webhook request
The provider waits on your CRM, times out, and retries. You get two leads. Store the event, return 200, and let the queue perform the POST. The duplicate key is what makes that retry safe.
A filter counted as a failure
Lea’s starter plan paints the workflow red. A filter that rejects the event is the status filtered, and later steps are skipped. The Zapier post already calls that a finished run.
The secret in the output
The run page prints the HMAC secret because the logger stored headers. Show the reference name. Keep the value out of the row you render.
A draft that changes a live run
You edit the CRM URL, and a slow queue would call the new host for evt-8841. The run pins revision 1 when it is created. The draft stays invisible until you publish revision 2.
A directory of apps
The editor grows a logo grid, an OAuth connect button, or a polling toggle. That is the month row. The non-goal names a connector marketplace, a branching canvas, arbitrary code, and polling triggers. One HTTPS URL is the action you have.
Deploy the hook
Whenhook wants Astro, D1, and a Worker queue. The hosting post is the adapter, the D1 binding, and the domain.
Point a test sender at /hooks/new-lead only after Ada’s signed sample posts once and the duplicate posts zero times. Then send Lea’s starter plan and confirm the CRM stayed quiet.
Replay, backoff, and a history search are the 80-hour row. The teardown lists Zapier’s autoreplay delays, from 5 minutes out to 6 hours, and a 60-day window. Leave that scheduler until a real run needs it. Then cancel Zapier if these pipes replace the ones you pay for, at $30–$69 a month.
What to skip on purpose
Leave out the replay button, automatic backoff, delay and email steps, a branching canvas, polling triggers, arbitrary code, and the connector directory.
A form that should start this hook is the Typeform guide. A typed table on the other side of the POST is the Airtable post. Whenhook is the pipe between them.
Start now
Go to Build your own Zapier. Copy the Astro prompt, or TanStack Start if that is what you write.
Tell the agent the first estimate. A webhook trigger, a transform step, and an HTTP action. One revision. One event key.
Publish New paid lead, send Ada’s event twice, and read the run page before you add a second destination.