Idempotency Keys in n8n: Stop Double-Charging, Double-Emailing, Double-Everything
Same webhook event in, same outcome out: an n8n idempotency key stops a timed-out provider retry from charging, emailing, or creating a CRM row twice.
William Spurlock Founder — Spurlock Studios Updated 16 MIN
If a webhook can fire twice — and it can — your workflow must be safe to run twice. That is all idempotency means in production: same event in, same business outcome out. One charge. One email. One CRM row.
n8n will not do this for you. The Webhook node starts a new execution on every delivery. Stripe will retry a live destination for up to three days if you are slow or return a non-2xx, and their own undelivered-event guide tells you to skip an already-processed event.id and still return 200. This spoke is the gate we put in front of irreversible nodes across 500+ automations. It sits inside the Production n8n handbook.
The short answer
- Key the event, not the run. Use the provider event ID (
evt_…, form response ID, CRM subscription ID). Never the n8n execution ID. - Claim the key before the write. Insert
processingwith a unique constraint, then charge / email / create. Store-at-the-end is a double waiting to happen. - Ack duplicates with 200. Stripe, HubSpot, and Typeform retry on timeout and on 4xx/5xx. A 500 after a successful charge is how you get a second charge.
- Two layers on money paths. Workflow key for the inbound event. Stripe
Idempotency-Keyon the outbound POST. - Test the double. Same payload twice. Replay an old execution. Partial-fail into a dead-letter queue, not a blind replay.
Why do webhooks fire twice when nothing is wrong?
Providers deliver at least once. That is the contract, not a bug in your graph.
| Cause | What you see | What the provider thinks |
|---|---|---|
| Slow graph, late 200 | Charge already landed, second delivery arrives | Timeout → retry |
| Network drop after success | Your 200 never reached them | Failed delivery → retry |
| Editor replay | You click Execute on an old run | A new execution with the same payload |
| Queue redelivery | Worker crashed after the write | At-least-once consumer |
| Manual resend | Someone hits Resend in a dashboard | A second delivery of the same event ID |
Stripe’s webhook docs are explicit: live destinations are retried for up to three days with exponential backoff. Sandbox retries three times over a few hours. Dashboard resend works for 15 days; the CLI resend works for 30. A manual resend does not cancel automatic retries already queued.
HubSpot’s error-handling docs retry failed webhook notifications up to 10 times over 24 hours on connection failure, a timeout longer than five seconds, or any 4xx/5xx. Typeform marks a delivery failed if you take longer than 30 seconds, then retries on a published backoff — and disables the webhook after enough 100% failures.
None of that requires a mistake in your nodes. It requires a gate before irreversible work.
- You know the provider’s retry window (hours vs days)
- You know which HTTP codes invite another delivery
- You return 200 on already-processed events
- You do not treat “the graph looks fine” as proof it ran once
What is an idempotency key in an n8n graph?
An idempotency key is a stable string that uniquely identifies a business event, not a workflow execution. You compute it from the payload, store it when you start processing, and skip side effects if you have seen it before.
Good keys:
evt_1NqqqL2eZvKYlo2C— Stripe event ID, reused on every retry of that eventtypeform_response_abc:submittedhubspot_deal_99:stage_won:2026-06-01T12:00:00Zwhen the same stage can fire more than once and each transition is real work
Bad keys:
- n8n
$execution.id— new every run, useless for provider retries - Timestamp-only keys — collisions and replay pain
- Entire raw body hashed —
updated_at, request IDs, and signature timestamps change on retry and the hash misses
| Key source | Detects provider retry? | Detects editor replay? | Safe for money? |
|---|---|---|---|
| Provider event ID | Yes | Yes, if payload kept | Yes |
| Hash of identity fields | Yes, if fields are stable | Yes | Yes, if you listed the fields |
| n8n execution ID | No | No | No |
Clock / now() | No | No | No |
| Full-body hash | Often no | Often no | No |
Write the formula on a sticky note on the canvas. Future you will forget which fields were identity and which were noise.
How does Stripe define the pattern you should copy?
Stripe published the pattern twice: once as API behavior, once as a design note. Copy both layers.
Inbound (their webhook → your n8n): the event object has a stable id (evt_…). Retries of that delivery reuse the same id. Stripe’s own recovery sample marks the event processing, does the work, marks it processed, and on a later delivery skips and still returns 200.
Outbound (your n8n → their API): you send an Idempotency-Key header on POST. Stripe saves the status code and body of the first request for that key — including 500s — and returns the same result on reuse. Keys are up to 255 characters. They suggest V4 UUIDs or another high-entropy string. They tell you not to put emails or other personal identifiers in the key. Keys are pruned after they are at least 24 hours old; reuse after prune starts a new request. GET and DELETE ignore the header because those methods are already idempotent.
| Layer | Key you send / store | What it stops |
|---|---|---|
| Inbound webhook | Stripe event.id in your table | Second CRM row / second email from a retry |
| Outbound Stripe POST | Idempotency-Key header | Second customer / second charge if your HTTP call times out |
| Both | Same string when it is the same operation | Timeout after they accepted the request |
Stripe also notes they do not save an idempotent result if validation fails before the endpoint runs, or if two requests with the same key collide while the first is still executing. Those cases are safe to retry. That is why a local processing vs completed state still matters even when the vendor has a header.
- Inbound key =
event.id(or equivalent) - Outbound Stripe POST carries
Idempotency-Key - You do not put PII in the key
- You know the vendor TTL (Stripe: 24 hours on the API key; three days on webhook retries)
How should the n8n Webhook node answer so retries slow down?
The Webhook node has four Respond modes. The mode you pick decides whether the provider is still waiting while you talk to HubSpot.
| Respond mode | When the caller gets a status | Use it when |
|---|---|---|
| Immediately | As soon as n8n accepts the request (“Workflow got started”) | You can finish work after the ack, and you have a key + DLQ |
| When Last Node Finishes | After the last node | The graph is short and you can beat the provider timeout |
| Using ‘Respond to Webhook’ Node | When that node runs | You need a 200 on the duplicate branch and after the first success |
| Streaming | Chunked, as nodes emit | Not a webhook-ack problem |
Respond to Webhook is the one that lets you return 200 on the “already completed” path without running the charge node. If the workflow errors before the first Respond to Webhook node, n8n returns 500. That 500 is a retry invitation. Put the respond node on the duplicate path and on the success path. Do not leave the only respond node after a flaky CRM call.
HubSpot times out at five seconds. Typeform at 30. Stripe will wait, then retry for days. If your graph cannot beat the shortest timeout in the mix, respond immediately after the key insert and finish the writes asynchronously — still behind the same key.
Verify the signature before you spend a key. That step lives in Webhook security for automations. An unverified body is not an event. It is noise you must not reserve a real key for.
What is the production gate that sits before side effects?
Minimal spine:
- Webhook receives the event (production URL, workflow published).
- Verify signature — see the security spoke.
- Code node builds the key from identity fields.
- Atomic insert — unique constraint. If the insert loses, this is a duplicate.
- Respond 200 on duplicate-complete. On duplicate-
processingolder than your stuck window, markneeds_review. - Only then run CRM / email / payment nodes.
- Mark
completedon success. On poison or ambiguous partial apply, send to the DLQ and do not delete the key.
Example key construction
const crypto = require("crypto");
const id = $json.id || $json.data?.id;
const version = $json.updated_at || $json.data?.updated_at || "v1";
if (!id) throw new Error("Missing event id for idempotency key");
const key = crypto.createHash("sha256").update(`${id}:${version}`).digest("hex");
return [{ json: { ...($json), idempotencyKey: key } }];
Use the provider’s event ID when they give you one. Fall back to a hash of stable identity fields when they do not. Throw if identity is missing — a missing key is not “skip the gate.”
Atomic claim (Postgres)
INSERT INTO automation_idempotency (key, workflow, state, execution_id)
VALUES ($1, $2, 'processing', $3)
ON CONFLICT (key) DO NOTHING
RETURNING key, state;
Empty RETURNING means another execution already claimed the event. Look up the existing row and branch. This is the concurrent-safe version of “check then insert.” Two Stripe deliveries arriving in the same second both lose the race except one.
| Step | Pass | Fail |
|---|---|---|
| Signature | Verified | 401 / drop, no key written |
| Key build | Stable event id present | Throw, no side effects |
| Insert | Row returned, processing | Duplicate branch |
| Side effects | Charge / email / CRM once | Never reached on duplicate-complete |
| Complete | State completed | DLQ + needs_review if the write may have landed |
How do you design keys by event type?
Create events (customer.created, form submitted)
Key = provider event ID. A retry of the same event no-ops. A second form from the same email is a different event — that is an upsert question, not the same key.
Update events (deal.updated)
Key = objectId + updated_at or objectId + version or objectId + hash(meaningful fields). Pure object ID is wrong if a later update should apply.
Action events (invoice.paid, payment_intent.succeeded)
Key = provider event ID. Money events almost always ship with stable IDs. Use them.
Synthetic events (cron sweeps)
Key = jobName + partition + date (for example renewal-reminders:2026-06-22). Overlapping cron runs are the quiet double-send.
| Event class | Key formula | Collision if you get it wrong |
|---|---|---|
| Create | providerEventId | Retry creates a second contact |
| Update | objectId + version | First update wins, later updates skip |
| Money action | providerEventId | Second capture / second receipt email |
| Cron | job + partition + date | Two reminder emails the same morning |
| Fan-out side effect | providerEventId:slack | CRM upserts, Slack double-pings |
Two different event types must not share a key. created and updated colliding is how a real update gets skipped.
Where should you store the keys?
Pick storage you already operate. The store has one job: a unique constraint you can insert against atomically.
| Store | Pros | Cons | Use it when |
|---|---|---|---|
| n8n Data tables | Lives next to the workflow | You still need a uniqueness rule you trust | Low volume, one project |
$getWorkflowStaticData | No extra service | Experimental; n8n says it is unavailable in tests and can misbehave under high-frequency runs; only saves on successful published executions | Last-seen cursor, not money |
| Postgres / Supabase | Unique constraint, audit, SQL janitor | You own schema and TTL | Default for charges and CRM creates |
Redis SET NX + TTL | Fast claim | Another moving part; worse audit | High-QPS intake in front of Postgres |
| Downstream “create if not exists” | Best when the API is already idempotent | Not all APIs are | Pair with a local key, do not replace it |
n8n documents static data as an experimental feature that does not persist during manual tests and may behave unreliably under high-frequency executions. That is a last-seen pointer for an RSS poll. It is not a charge gate.
Prefer native idempotency headers when Stripe-like APIs offer them and still keep your own key for the rest of the workflow. One vendor header does not cover Slack, Gmail, or a spreadsheet append.
Why is a boolean “seen” flag not enough?
A boolean cannot tell a clean retry from a crash after the charge. Prefer a small state machine.
| State | Meaning | On retry |
|---|---|---|
processing | Work started, not confirmed done | Wait, or needs_review if stuck past your window |
completed | Side effects committed | No-op 200 |
failed_clean | Failed before any side effect | Allow retry; do not keep a completed mark |
needs_review | Ambiguous partial apply | Human only — DLQ, do not auto-replay creates |
Implement with a row: key, state, executionId, updatedAt. TTL or a janitor clears old completed rows. Never auto-delete needs_review.
If you only store keys at the end, a crash after the charge creates a double on the next delivery. If you only store at the start without states, a crash blocks legitimate retries forever. States fix both.
Stripe’s own sample uses the same idea: is_processing_or_processed, mark_as_processing, mark_as_processed. They are not decorating a blog post. They are telling you the race is real.
- Unique constraint on
key -
processingwritten before the first irreversible node - Stuck-
processingalert (we use 10–15 minutes as a starting window) -
needs_reviewnever auto-replays a create
How do you prevent duplicate runs without breaking retries?
When you detect a duplicate, still return success if you already processed the event. Returning 500 invites more retries and more noise. Stripe says this in the undelivered-event guide: ignore the already-processed event and return a successful response so automatic retries stop.
| Situation | Store | HTTP back to provider | Side effects |
|---|---|---|---|
| Key unseen | Insert processing | 200 after accept or after success | Run once |
Key seen + completed | No write | 200 | None |
Key seen + processing (fresh) | No write | 200 “already accepted” | None — the other run owns it |
Key seen + processing (stuck) | Flip needs_review | 200 | None — human |
| Failed before any side effect | failed_clean or delete the claim | 500 only if nothing landed | Retry allowed |
| Side effect maybe applied, then crash | needs_review + DLQ | 200 | No blind re-create |
This is why idempotency and dead-letter queues travel together. Keys stop doubles. DLQs handle the ambiguous middle. The handbook treats both as structure, not optional polish.
Do not return 500 to “make them retry” after a charge node has already returned success. You cannot un-charge by being honest about a later Slack failure. Park Slack. Ack the event.
Which side effects always need a key check?
Put the check before:
- Payment capture, refund, or invoice send
- Outbound email / SMS / Slack to a customer or a sales channel that treats volume as signal
- CRM create (updates need an upsert rule, not a blind create)
- Spreadsheet append rows
- “Increment counter” analytics writes
- Ticket creation
| Side effect | Naturally idempotent? | What doubles cost |
|---|---|---|
| Stripe charge / invoice | Only with Idempotency-Key | Money |
| Gmail / transactional email | No | Trust, unsubscribes |
Slack to #sales | No | Two “new leads,” two owners |
| HubSpot contact create | No, unless you upsert on email / external ID | Duplicate records, broken attribution |
| HubSpot property write to the same value | Mostly | Noise, rate-limit burn |
| Google Sheet append | No | Two rows, two downstream automations |
| Ticket create | No | Two agents on one incident |
Updates that set stage to won with the same payload are safer. They still benefit from dedupe so you do not burn rate limits and so your metrics stay honest.
The gate goes before the first of these nodes. A gate after the email node is a comment.
Is n8n Remove Duplicates enough?
The Remove Duplicates node can drop items seen in previous executions. Set Operation to Remove Items Processed in Previous Executions, Keep Items Where to Value Is New, and Value to Dedupe On to the provider event ID. Default history size is 10,000 items. Scope is per node or per workflow. n8n overhauled the node in 1.64.0 — read the current docs if your instance is older.
Use it for serial, low-stakes dedupe: a content poll, a “don’t re-notify this issue id” filter.
Do not use it as the only gate on a charge.
| Tool | Atomic under two parallel deliveries? | Audit trail | Money path? |
|---|---|---|---|
| Remove Duplicates | Not a database unique insert — two executions can both see “new” before either records history | History in n8n, not your ledger | No |
| Read-then-IF in a Code node | No — classic race | Only if you log | No |
INSERT … ON CONFLICT DO NOTHING | Yes | Your table | Yes |
Redis SET key value NX EX seconds | Yes, as a claim | Weak unless you also log | Claim only, then persist |
Remove Duplicates is a filter. A unique constraint is a lock. Money and customer-visible email need the lock.
How do you coordinate with downstream Idempotency-Key headers?
Some APIs accept Idempotency-Key. Use the same string you store locally when the outbound call is the business event (create this invoice for this webhook). That way a network timeout after the provider accepted the request still safe-retries.
When the API has no such header, make creates into upserts keyed by natural identity (email, external ID, Stripe customer id). Workflow-level key plus upsert is the usual production pair.
| Downstream | Native key? | What you send | What you still store locally |
|---|---|---|---|
| Stripe POST | Yes — Idempotency-Key | Your event key or a UUID you persist | Inbound event.id + outbound key |
| HubSpot contact | Upsert by email / external ID | No header | Event key so Slack does not double |
| Gmail / transactional | Usually no | Nothing they will honor | Event key, or eventId:email |
| Slack | No | Nothing | eventId:slack |
| Sheet append | No | Nothing | Event key — append is never safe to replay |
Do not invent a new UUID for every retry of the same Stripe POST. A new key is a new charge. Persist the outbound key next to the inbound event id on the first attempt, then reuse it.
Stripe will error if you reuse a key with different parameters than the original request. That is protection against accidental misuse. If the payload changed, you need a new key because it is a new operation.
What happens when one event fans out to two workflows?
One inbound event sometimes triggers a CRM graph and a notify graph. Options:
- Single intake that fans out internally after the key gate (simplest).
- Per-workflow keys with a suffix:
evt_123:crm,evt_123:email— if each side effect must be independently replayable. - Shared gate sub-workflow that every graph calls first.
| Design | Replay Slack without re-charging? | Risk |
|---|---|---|
| One key, one graph | No — the whole graph is one unit | Simple, coarse |
| Suffixed keys | Yes | You must still share the charge key across graphs |
| Two graphs, two incomplete keys | Accidental | “We deduped Slack but double-charged” |
Avoid two workflows both checking different incomplete keys against the same charge API. Suffix the side effects. Keep one key for the money.
If you must split graphs, the charge graph owns evt_123 (or evt_123:charge). The notify graph owns evt_123:slack and is allowed to run only after the charge row is completed, or it upserts and never creates.
Worked example: Stripe event → charge follow-up → CRM → email
A payment_intent.succeeded webhook should: record the event, upsert the customer in the CRM, send one receipt email. The payment already happened in Stripe. Your job is to not fork the rest of the business.
Without a gate: Stripe retries after a slow HubSpot upsert → second contact → second receipt → support hears “you billed me twice” even when Stripe only captured once.
With a gate:
- Verify the Stripe signature. Drop unsigned bodies. Do not spend a key on them.
- Key =
event.id(evt_…). INSERT … ON CONFLICT DO NOTHINGasprocessing. Conflict +completed→ Respond to Webhook 200. Conflict +processingolder than 15 minutes →needs_review.- Optional outbound Stripe calls (create a customer, attach metadata) use
Idempotency-Key: evt_…:customerso a timeout does not create two customers. - Upsert HubSpot by email with the Stripe customer id as external ID.
- Send the receipt once. If you must retry the email later, key it
evt_…:receiptso the CRM upsert can run again and the inbox cannot. - Mark
completed. Respond 200 if you have not already.
Even if step 6 fails after step 5, replay uses the HubSpot upsert and the suffixed email key. The CRM does not fork. The customer does not get two receipts.
Write this story into the runbook. New teammates ship safer graphs when they can see a concrete path.
Storage schema you can copy
create table automation_idempotency (
key text primary key,
workflow text not null,
state text not null,
execution_id text,
business_id text,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
create index on automation_idempotency (updated_at);
create index on automation_idempotency (state, updated_at);
Janitor: delete completed rows older than 30 days; alert on processing older than 15 minutes; never auto-delete needs_review. Finance-adjacent events may need the row longer than the hot dedupe TTL — keep an audit copy even if you prune the hot table.
How do you test duplicates and catch the usual mistakes?
Before go-live:
- Fire the same webhook payload twice in sixty seconds.
- Confirm one set of side effects (one charge follow-up, one email, one CRM row).
- Confirm the provider-facing response stays 200 on both deliveries.
- Replay an old execution in n8n and confirm the gate holds.
- Fail the workflow after the key is
processingand confirm the DLQ path is sane — no deleted key, no blind create. - Fail after the CRM upsert and before email; replay; confirm upsert + one email.
| Drill | Pass | Fail |
|---|---|---|
Double POST, same event.id | One CRM row, two 200s | Two rows or a 500 on the second |
| Editor replay | Gate holds | Second Slack ping |
| Crash after charge, before store | You cannot pass this if you store last | This is the production double |
| Crash after store, before charge | Retry allowed or failed_clean | Stuck processing forever |
| Partial CRM + failed email | Suffixed email key, no second contact | Second contact |
If you have never tested a double delivery, you do not have idempotency. You have a comment in a Notion doc.
Mistakes that create the double you thought you blocked
- Keying on the whole body including volatile fields (timestamps that change, new signatures on retry) — duplicates miss.
- Storing the key only at the end — crash after the charge, retry, second charge.
- Shared keys across event types —
createdandupdatedcollide and skip real work. - No TTL — store grows forever; use retention that matches replay windows (often 7–30 days).
- Dedupe after the email node — entertaining, useless.
- New Stripe
Idempotency-Keyon every retry — you opted out of their cache. - 500 after a successful write — you asked for another delivery.
- Static data as the money gate — n8n will not even persist it in the editor test you used to convince yourself.
Observability worth a weekly look
| Metric | Spike usually means |
|---|---|
| Duplicate hits (key seen → no-op) | Retry storm or your endpoint got slower than the provider timeout |
| First-time processes | Traffic; compare to provider event volume |
needs_review volume | Partial applies — fix those graphs before you scale |
Time stuck in processing | A node is hanging or a crash path does not resolve state |
| 5xx responses from the Webhook | You are inviting the next retry |
A sudden spike in duplicates is often latency, not a new attacker. A spike in needs_review is a graph that writes, then dies.
When is “exactly once” a myth?
You cannot get mathematically perfect exactly-once across arbitrary SaaS APIs. You can get:
- At-least-once delivery from the provider
- Effectively-once business outcomes via keys + upserts + careful replay
Anyone selling “exactly once webhooks” without those pieces is selling a slogan. Design for the myth. Implement for the outcome: same event in, same charge / email / CRM row out.
Checklist before enabling a new trigger
- Identity field documented
- Key formula written on the canvas
- Store supports a unique constraint
- Key claimed as
processingbefore the first irreversible node - Duplicate test passed (same payload twice → one outcome, two 200s)
- Partial-fail path defined (DLQ, not delete-and-replay)
- TTL / janitor exists
- Metrics for duplicate hits enabled
- Webhook respond mode beats the provider timeout
- Signature verification sits in front of the key (see webhook security)
Ship the checklist with the workflow. Skip it and you will meet the duplicate in production first. The automation lane is where we install this as a standard, not a one-off Code node.
FAQ
What is n8n idempotency?
It is the practice of giving each business event a stable key and skipping irreversible nodes when that key was already processed. n8n starts a new execution on every Webhook delivery and does not apply this automatically. You build the check — usually a unique insert plus a 200 on the duplicate path.
How do I prevent duplicate webhook runs?
Verify the webhook, compute a key from the provider event identity, insert it atomically, and exit successfully if the insert loses and the prior state is completed. Only then run creates, charges, or outbound messages. Test by sending the same payload twice and confirming one set of side effects.
Should I use the n8n execution ID as the key?
No. Every execution gets a new ID, including provider retries and editor replays. That cannot detect a second delivery of evt_123. Use the external event ID or a hash of stable business identity fields.
What if the API already supports idempotency keys?
Use them — Stripe’s Idempotency-Key on POST is the right outbound lock for charges and object creates. Also keep your workflow-level key for nodes that do not support native idempotency (email, Slack, spreadsheets, secondary CRMs). One header does not cover the rest of the graph.
How long should I keep keys?
Long enough to cover provider retry windows and your own manual replays — commonly 7 to 30 days. Stripe retries live webhooks for up to three days and lets you resend from the CLI for 30. Finance-adjacent events may need longer audit retention even if the hot dedupe TTL is shorter.
What about partial failures after a key is stored?
Send the item to a dead-letter queue with the original payload and mark it needs_review. Do not delete the key and blindly replay if a charge or create may have succeeded. Replay through upserts and suffixed keys so the CRM row and the email can be completed independently.
CTA
Duplicate side effects are not an edge case. They are a calendar event.
Build the key gate into the next n8n workflow that can charge, email, or create a CRM row. Keep the production handbook open while you wire it. When you want this installed as a standard, start at automation or book the $500 Automation Audit.
What questions does this article answer?
- What is n8n idempotency?
- It is the practice of giving each business event a stable key and skipping irreversible nodes when that key was already processed. n8n starts a new execution on every Webhook delivery and does not apply this automatically. You build the check — usually a unique insert plus a 200 on the duplicate path.
- How do I prevent duplicate webhook runs?
- Verify the webhook, compute a key from the provider event identity, insert it atomically, and exit successfully if the insert loses and the prior state is `completed`. Only then run creates, charges, or outbound messages. Test by sending the same payload twice and confirming one set of side effects.
- Should I use the n8n execution ID as the key?
- No. Every execution gets a new ID, including provider retries and editor replays. That cannot detect a second delivery of `evt_123`. Use the external event ID or a hash of stable business identity fields.
- What if the API already supports idempotency keys?
- Use them — Stripe's `Idempotency-Key` on POST is the right outbound lock for charges and object creates. Also keep your workflow-level key for nodes that do not support native idempotency (email, Slack, spreadsheets, secondary CRMs). One header does not cover the rest of the graph.
- How long should I keep keys?
- Long enough to cover provider retry windows and your own manual replays — commonly 7 to 30 days. Stripe retries live webhooks for up to three days and lets you resend from the CLI for 30. Finance-adjacent events may need longer audit retention even if the hot dedupe TTL is shorter.
- What about partial failures after a key is stored?
- Send the item to a dead-letter queue with the original payload and mark it `needs_review`. Do not delete the key and blindly replay if a charge or create may have succeeded. Replay through upserts and suffixed keys so the CRM row and the email can be completed independently.
Last reviewed
Automation
Automation After the show is not you at 1 a.m.
Post-show onboarding — thank-you, join path, merch nudge — belongs in a human-gated n8n rail, not your thumb at load-out.
Automation Paperwork that is not the plant
Invoice and PO matching, intake, and support triage in n8n with Metrc fences — the paperwork operators hate, not a menu widget.
Automation Saturday still books — the missed-call rail for trades
A missed-call text-back that routes zip and books a slot beats voicemail and Saturday desk coverage you cannot keep staffed. If a kid is cheaper, say so.
Automation Why doesn’t worker concurrency cap my n8n sub-workflows
Worker concurrency does not cap n8n sub-workflows. Each Execute Workflow child is a new execution the production limit skips, usually on the parent worker.
Will's Journal in your inbox.
What I learned this week building for shops, floors, and houses.
You're on the list.
Sign-up failed — try again.
By subscribing, you agree to the Privacy Policy.