automation-flows
Use when building or fixing a no-code automation on n8n, Make, or Zapier — trigger to multi-app steps with data mapping, dedup, retries and an error path — or picking the platform by billing unit (task vs credit vs execution). NOT a typed API client in code (that is api-connector
#workflows #integrations #automation
Install
npx skills add https://github.com/ericrisco/rsc-harness/tree/main/skills/automation-flows
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install ericrisco-rsc-harness@llmmart
git clone https://github.com/ericrisco/rsc-harness.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole ericrisco/rsc-harness collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Automation flows — glue many SaaS apps on a visual platform, with an error path that actually fires
You are building a working automation on a hosted visual platform: a trigger, a chain of app actions with branching and explicit data mapping, and an error-handling and retry strategy without which the flow is not done.
Your job is two things at once: platform judgement (pick n8n vs Make vs Zapier by the constraints) and a buildable artifact (an importable n8n workflow JSON, or a precise numbered build sheet for Make/Zapier, which have no portable export).
This skill stops the moment the right answer is real code. Writing a typed API client → ../api-connector-builder/SKILL.md. Building the endpoint that receives a webhook in your own app → ../webhooks/SKILL.md. Scripting one vendor directly → ../stripe/SKILL.md, ../notion-connector/SKILL.md, ../google-workspace/SKILL.md, ../whatsapp-telegram/SKILL.md.
1. Pick the platform
The single most expensive mistake is choosing on familiarity instead of cost model. The three platforms bill on fundamentally different units, and at volume that gap is 10×.
| Constraint | Zapier | Make | n8n |
|---|---|---|---|
| Billing unit (why it dominates cost) | per task — every action counts | per credit — each module action = 1 credit (was "operations" until 2025-08-27; converted 1:1) | per execution — whole run = 1, any step count |
| Free tier | 100 tasks/mo | 1,000 ops/mo | self-host free, unlimited execs |
| Entry paid | Pro ≈ $19.99/mo (billed annually), 750 tasks | Core ≈ $9/mo, 10k credits (billing unit became credits on 2025-08-27) | cloud Starter ≈ €20/mo (billed annually), 2,500 execs; self-host = $0 |
| App breadth (obscure-app signal) | ≈ 8,000+ integrations — widest | ≈ 1,500, often deeper per app | ≈ 1,000 nodes + generic HTTP node + code |
| Self-host / data residency | no | no | yes — your infra, your data |
| Who maintains it | non-technical-friendly | mid; visual but richer | technical; you run the box (n8n 2.0, stable Dec 2025, made isolated code execution the default — Code nodes run in sandboxed task runners) |
Worked cost example. A 10-step flow run 10,000×/month:
- Zapier: ~100,000 tasks (10 actions × 10k) → well past the Pro tier, into the high tiers.
- Make: ~100,000 credits → similar pressure.
- n8n: 10,000 executions regardless of step count; self-hosted = $0.
For complex, high-volume flows, n8n's execution model can cut cost 80–90% vs Zapier.
Pricing and version figures move; re-check the primary vendor pages before quoting a customer (the ≈ is a hedge, not a guarantee): Zapier zapier.com/pricing, Make make.com/en/pricing, n8n n8n.io/pricing, and the n8n 2.0 release note blog.n8n.io/introducing-n8n-2-0. Make's switch to credits as the billing unit (2025-08-27) and n8n 2.0's sandboxed-by-default code execution (stable Dec 2025) are the two facts most likely to surprise someone who learned these tools a year ago.
Decision in one line per row: bill on the unit that matches your shape — many short flows favor task/op platforms; few long flows favor n8n. Obscure app you can't find a node for → Zapier. Raw HTTP / custom code / data must stay on your infra → n8n. Non-technical owner who never wants to SSH → Zapier or Make cloud.
2. Anatomy: trigger → steps → output
A flow has exactly one trigger. Then a chain of action steps. Map every field explicitly.
Trigger: prefer webhook/push over polling. A webhook trigger (Zapier Catch Hook, Make custom webhook, n8n Webhook node) fires on an inbound POST — near-instant. A polling trigger (Zapier Retrieve Poll) does a periodic GET; the interval depends on plan, 1–15 minutes between checks. Polling costs latency, costs runs (it fires even when nothing changed), and can miss events between polls.
Bad: Trigger = "poll Airtable for new rows every 15 min" → up to 15 min stale, burns runs on empty checks
Good: Trigger = Airtable "new record" webhook → fires the instant the row lands, zero idle runs
Map data explicitly. Never assume field names survive a hop. The Typeform field email does not arrive at the Slack step called email — it arrives as a node-output reference you must wire by hand. Pin a real sample, look at the actual output keys, map from those.
Add a guard early. Put a filter/condition right after the trigger so junk events stop before they hit an external API: drop test payloads, require the fields you need to be non-empty, exit on the wrong event type.
3. Error handling — the spine
Every flow ships with an error path, because a flow without one is a silent failure waiting for the day the API hiccups and nobody notices the orders stopped syncing. Before you call a flow done you must be able to point at three things: where a failed run goes, how many times it retries, and who gets told. Full per-platform recipes (including the manual exponential-backoff loop) live in references/error-handling.md, and the retry/backoff theory under them in ../error-handling/SKILL.md; here is the working core.
n8n. Build a dedicated Error Workflow that begins with the Error Trigger node — it runs only when a monitored workflow fails. Wire it to Slack/email/a log row, then set it as the main flow's settings.errorWorkflow. On risky nodes (anything hitting an external API) toggle Retry On Fail (Max Tries 3–5, set a Wait between tries) and, where a single failed item shouldn't kill the run, Continue On Fail (the node emits an error object instead of halting). n8n's built-in retry is linear — for true exponential backoff you build a wait/loop yourself (recipe in references).
Make. Attach an error handler to the risky module:
- Break — the production default. Sends the failed run to the Incomplete Executions queue (no data loss) and can auto-retry from there.
- Resume — supply a hard-coded fallback value and continue.
- Ignore — continue past a non-critical failure.
- Commit — end marked success. Rollback — end marked error and try to revert (not all modules support revert → can leave inconsistency).
- Always put a filter before any external-API module to validate data first.
Zapier. Autoreplay automatically replays failed steps, up to 5 retries per step — but it's account-wide and turns OFF for a Zap once that Zap is published with its own custom error handling. Filters gate a Zap so it only proceeds when data is the right shape. Paths give if/then branching, including a fallback branch on error.
| Concern | n8n | Make | Zapier |
|---|---|---|---|
| Auto-retry | Retry On Fail (Max Tries 3–5, linear) | Break → Incomplete Executions auto-retry | Autoreplay (5/step, account-wide) |
| Don't halt on one bad item | Continue On Fail | Ignore / Resume | Filter to skip |
| Branch / fallback | IF + Error Workflow | router + Resume | Paths |
| Failure alert | Error Trigger → Slack/email | error handler → notify module | published Zap error notification |
| Exponential backoff | manual wait/loop | manual | not native |
4. Idempotency & dedup
Flows commonly run twice for one event: providers deliver webhooks at-least-once, and retries replay. If your flow does a non-idempotent write (create a charge, send an email, insert a row), a double-fire means a double charge or a duplicate record.
Fix: dedup on a stable key (the event id / external id) before any non-idempotent action.
- n8n — a check-before-write node or DB lookup keyed on the id; skip if seen.
- Make — a data store keyed on the id; check, then write the key.
- Zapier — a storage/lookup step (Storage by Zapier) keyed on the id; filter out if present.
Bad: webhook → create Notion row (Stripe retries the event → two rows)
Good: webhook → lookup event_id in store →
filter "not seen" → create Notion row → save event_id
5. Test & observe before publish
- Pin a real sample payload (or use the platform's test execution) — don't reason about field names blindly.
- Deliberately fire the error branch: force a bad value, watch the failed run land where you expect.
- Confirm the alert actually arrives — send the test Slack/email and see it in the channel, not just "it should fire".
- Re-send the same event and confirm the dedup guard blocks the second run.
- Only then publish. (On Zapier, remember publishing with custom error handling turns Autoreplay off for that Zap.)
6. Emit the artifact
If n8n is chosen, produce an importable workflow JSON the user can paste into Import from File/Clipboard. It must have a non-empty nodes array (including a trigger node), a connections object, and settings.errorWorkflow pointing at the Error Workflow. Reference credentials by the n8n credential store, never paste secrets inline. Full schema and a minimal trigger→action→error example: references/n8n-workflow-json.md.
If Make or Zapier is chosen, produce a numbered build sheet — neither has a portable export you can hand over. One row per step: # | app | action | field mapping (source → target) | error directive. End with the trigger type and the dedup key.
Run scripts/verify.sh on any JSON you emit: read-only, no network, no credentials — it parses the file and checks a non-empty nodes array, a connections object, and ≥1 trigger node (exits 0 on an empty target).
Anti-patterns
| Anti-pattern | Why it bites | Do instead |
|---|---|---|
| No error path | First API hiccup, the flow dies silently; you find out from an angry customer | Wire the platform's error handler + a real alert before shipping |
| Polling when a webhook exists | 1–15 min stale, burns runs on empty checks | Use the push/webhook trigger |
| 12-step branching logic crammed into Zapier | Task billing explodes; logic gets unmaintainable | Move complex/high-volume logic to n8n |
| Blind field mapping | email ≠ the field the next step calls email; data silently lands empty |
Pin a sample, map from real output keys |
| Non-idempotent write, no dedup | At-least-once delivery → double charge / duplicate row | Dedup on event id before the write |
| Secrets pasted inline in a node | Leaked in exports, unrotatable, shared everywhere | Use the platform credential store, reference by name |
| One mega-flow doing everything | Unreadable, untestable, one failure nukes all | Split: trigger → sub-flow per concern |
| Choosing platform by familiarity | Bill 10× higher than the right unit; "my automation bill exploded" | Pick by billing unit (task vs op vs execution) up front |
Files (rsc-harness)
-
evals
-
cases.yaml 3.3 KB
skill: automation-flows should_trigger: - prompt: "Build an n8n workflow: new Typeform response → add a row to Google Sheets → notify a Slack channel." why: Canonical multi-app visual flow with a trigger and chained app actions — the core deliverable. - prompt: "My Zap keeps failing silently at the HTTP step. Add retries and an alert so I know when it breaks." why: Error handling on a visual platform — Autoreplay/Filters plus a failure notification. - prompt: "Should I use Make or Zapier if I run about 50k automations a month?" why: Platform selection by billing model (task vs operation) — non-obvious, not a build request but squarely this skill. - prompt: "por qué se me dispara el Zap dos veces por cada pago de Stripe" why: Spanish; the double-fire symptom routes to idempotency/dedup on a stable event id. - prompt: "munta un workflow a n8n que processi els emails entrants amb gestió d'errors" why: Catalan; n8n flow with an explicit error-handling requirement. - prompt: "Give me the JSON I can import into n8n for a webhook → Notion → email flow." why: Non-obvious artifact request — the user wants the importable workflow JSON, the checkable deliverable. - prompt: "My automation bill exploded last month — I have a 12-step Zap running thousands of times." why: Cost symptom pointing at the per-task billing model and a move to n8n's per-execution model. should_not_trigger: - prompt: "Write a TypeScript client for the Acme REST API with auth, pagination, and exponential backoff." route_to: api-connector-builder why: That is real code — a typed client — not a visual no-code flow. - prompt: "Build the endpoint in my Express app that receives and verifies Stripe webhook signatures." route_to: webhooks why: Building the inbound receiver in your own app code; this skill consumes webhooks as triggers, it does not build the receiver. - prompt: "Append rows to a Google Sheet from a Python script every night." route_to: spreadsheet-ops why: Scripting the spreadsheet surface directly in code, not chaining SaaS apps on a platform. - prompt: "Send a WhatsApp template message to customers from my backend service." route_to: whatsapp-telegram why: Single-vendor SDK called from your own code, not a visual flow. - prompt: "Scrape product prices off this site into a CSV." route_to: data-scraper why: Web extraction/scraping, not gluing apps together with triggers and actions. capability: - scenario: "User wants an n8n flow: Stripe payment_succeeded webhook → create a Notion DB row → post a Slack message, with production-grade error handling. Produce the importable workflow JSON and explain the error/dedup design." must_include: - "Exactly one trigger, and it is a webhook (not a polling trigger)." - "An importable n8n workflow JSON with a non-empty `nodes` array and a `connections` object." - "A dedicated Error Workflow via an Error Trigger node wired to a notification, referenced by `settings.errorWorkflow`." - "Retry On Fail set on the external-API node (Max Tries 3–5) with a wait between tries." - "Idempotency/dedup on the Stripe event id checked before the Notion write." - "Credentials referenced via the n8n credential store, never pasted inline." -
README.md 1.2 KB
# Evals — automation-flows `cases.yaml` holds three groups. `should_trigger` lists prompts where this skill must activate (canonical multi-app flows, error-handling fixes, platform-selection-by-billing, the double-fire/dedup symptom, and an importable-JSON request, with Spanish and Catalan phrasings). `should_not_trigger` lists adjacent prompts that belong to a named sibling (code clients → api-connector-builder, webhook receivers → webhooks, sheet scripting → spreadsheet-ops, single-vendor SDK calls → whatsapp-telegram, scraping → data-scraper). `capability` is one end-to-end scenario with a `must_include` rubric the produced flow has to satisfy. There is no automated runner here. Score by judgement: feed each `should_trigger` / `should_not_trigger` prompt to the routing layer and confirm it activates (or routes to the listed sibling). For `capability`, have the skill produce the n8n flow, then check every `must_include` item by hand and run `scripts/verify.sh <dir-with-the-json>` to confirm the emitted workflow JSON parses, has non-empty `nodes`, a `connections` object, and a trigger node. `verify.sh` is read-only and exits 0 on an empty target.
-
-
references
-
error-handling.md 4.4 KB
# Error handling & dedup — per-platform deep dive This expands section 3–4 of `../SKILL.md`. Read it when you are actually wiring the failure path, not before choosing a platform. ## n8n ### Error Workflow + Error Trigger 1. Create a second workflow. Its first node is the **Error Trigger** node — it does not run on its own, only when a monitored workflow fails. 2. After the Error Trigger, add a notification node (Slack, Send Email) and/or a log row (append to a sheet/DB). The Error Trigger output carries `execution.id`, `execution.url`, the failed node name, and the error message — put those in the alert so it's actionable. 3. On the **main** workflow, set this Error Workflow under **Settings → Error Workflow** (serialized as `settings.errorWorkflow` in the exported JSON). ### Per-node resilience - **Retry On Fail** (node Settings toggle): set **Max Tries 3–5** and a **Wait Between Tries** (e.g. 1000–5000 ms). Use on every node that hits an external API. - **Continue On Fail** (toggle): the node emits an error object instead of halting the run. Use when one bad item in a batch should be skipped/branched, not fatal. Branch on the error output with an IF node. ### Manual exponential backoff n8n's built-in retry is **linear** (fixed wait). For exponential backoff, build it: ```text [HTTP Request] --(on error / Continue On Fail)--> [IF: attempts < max] --true--> [Set: attempt = attempt + 1] --> [Wait: 2 ^ attempt seconds] # 2s, 4s, 8s, 16s... --> back to [HTTP Request] --false--> [Error path: alert + give up] ``` Cap the attempts (e.g. 5) and the max wait so a hard-down API can't loop forever. ## Make Attach a handler by right-clicking the risky module → **Add error handler**. | Handler | Effect | Use when | | --- | --- | --- | | **Break** | Pause the run, send it to **Incomplete Executions**; auto-retry per the scenario's retry settings | Default for production — no data loss, recoverable | | **Resume** | Replace the failed module's output with a hard-coded value and continue | A sane fallback exists (default tag, empty result) | | **Ignore** | Skip the failure, continue the route | The step is non-critical (a nice-to-have notification) | | **Commit** | End the run immediately, marked success | You've already done the important work | | **Rollback** | End marked error and try to revert prior modules | You need all-or-nothing — but **not all modules support revert**, so verify, or you leave partial state | Always put a **Filter before** any external-API module to drop malformed data before it triggers an error in the first place. Tune the scenario's **auto-retry** settings (attempts + interval) for the Incomplete Executions queue. ## Zapier - **Autoreplay** — account-wide setting that auto-replays failed steps, up to **5 retries** per step. Caveat: once a Zap is **published with its own custom error handling**, Autoreplay turns OFF for that Zap, so you own the retry logic from then on. - **Filters** — gate the Zap so it only continues when fields are present and the right shape. Use as a guard right after the trigger and before any irreversible action. - **Paths** — if/then branching. Add an explicit fallback Path for the error/edge case so the Zap degrades gracefully instead of erroring out. - **Sub-Zaps** — extract shared logic; keep each Zap small to keep task billing and debugging sane. ## Dedup / idempotency patterns Why: webhooks are delivered **at-least-once** and retries replay events. A non-idempotent action (charge, email, insert) must be guarded by a stable key — the provider's **event id** or your **external id**. - **n8n** — before the write, a lookup node (DB query, or an HTTP GET against your store) on the event id; route through an IF so "already seen" exits. After a successful write, persist the id. - **Make** — use a **Data store**. `Get a record` by event id → `Filter: record not found` → do the write → `Add/Replace a record` with the id. The data store is the dedup ledger. - **Zapier** — **Storage by Zapier**: `Get Value` by event id → Filter `does not exist` → action → `Set Value` of the id. Or look up a row in a sheet/DB keyed on the id. Always: **check the key before the write, persist the key after the write.** If the write isn't naturally idempotent and your store write can fail independently, prefer a single upsert keyed on the id where the target app supports it. -
n8n-workflow-json.md 3.1 KB
# n8n workflow JSON — schema, example, import, verification When n8n is the chosen platform, your checkable artifact is a workflow JSON the user imports. This is the shape `scripts/verify.sh` validates. ## Required top-level shape ```json { "name": "Stripe payment → Notion → Slack", "nodes": [], "connections": {}, "settings": { "errorWorkflow": "<error-workflow-id-or-name>" } } ``` - **`nodes`** — non-empty array. Each node has `id`, `name`, `type` (e.g. `n8n-nodes-base.webhook`), `typeVersion`, `position`, and `parameters`. Exactly one node is a trigger (its `type` ends in `webhook`, `Trigger`, `cron`, etc.). - **`connections`** — object mapping a source node name → its outputs → the downstream nodes. An empty object is valid JSON but means nothing is wired; a real flow has entries. - **`settings.errorWorkflow`** — points at the Error Workflow (see `error-handling.md`). Without it, failures don't alert. - **Credentials** — nodes reference a credential by id/name from the n8n credential store via a `credentials` object. **Never** inline an API key or token in `parameters`. ## Minimal trigger → action → error example ```json { "name": "Webhook → Notion (with dedup + retry)", "nodes": [ { "id": "1", "name": "Webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 2, "position": [0, 0], "parameters": { "httpMethod": "POST", "path": "stripe-payment" } }, { "id": "2", "name": "Dedup lookup", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [240, 0], "parameters": {} }, { "id": "3", "name": "Create Notion row", "type": "n8n-nodes-base.notion", "typeVersion": 2, "position": [480, 0], "parameters": { "resource": "databasePage", "operation": "create" }, "retryOnFail": true, "maxTries": 4, "waitBetweenTries": 2000, "credentials": { "notionApi": { "id": "5", "name": "Notion account" } } } ], "connections": { "Webhook": { "main": [[{ "node": "Dedup lookup", "type": "main", "index": 0 }]] }, "Dedup lookup": { "main": [[{ "node": "Create Notion row", "type": "main", "index": 0 }]] } }, "settings": { "errorWorkflow": "Global error alerter" } } ``` The trigger is the `webhook` node (not polling). `retryOnFail`/`maxTries`/`waitBetweenTries` live on the external-API node. Credentials are referenced by store id/name. `settings.errorWorkflow` names the separate Error Workflow. ## How the user imports it 1. n8n → **Workflows → Import from File** (or paste via *Import from Clipboard*). 2. Open each node with a `credentials` reference and pick/create the matching credential in the store. 3. Activate the workflow; copy the Webhook node's production URL into the upstream provider. ## What verify.sh checks `scripts/verify.sh` runs read-only over any `*.json` in the target path and asserts each one: parses as JSON, has a non-empty `nodes` array, has a `connections` object, and has at least one trigger node (a node whose `type` ends in `webhook`/`Trigger`/`cron` or `trigger`). It makes no network calls and needs no credentials. With no JSON files present it exits 0 (nothing to check is not a failure).
-
-
scripts
-
verify.sh 2.2 KB
#!/usr/bin/env bash # verify.sh — read-only validator for n8n workflow JSON emitted by automation-flows. # Checks every *.json under the target: parses as JSON, non-empty `nodes` array, # `connections` object, and >=1 trigger node. No network, no credentials. # Exits 0 when there is nothing to check (empty/clean target is not a failure). set -euo pipefail TARGET="${1:-.}" if [ ! -e "$TARGET" ]; then echo "verify: target not found: $TARGET (nothing to check)" exit 0 fi # Collect candidate JSON files (files or a directory tree). files=() if [ -d "$TARGET" ]; then while IFS= read -r f; do files+=("$f"); done < <(find "$TARGET" -type f -name '*.json' 2>/dev/null) elif [ -f "$TARGET" ]; then files+=("$TARGET") fi if [ "${#files[@]}" -eq 0 ]; then echo "verify: no *.json workflow files found under $TARGET (nothing to check)" exit 0 fi PY="$(command -v python3 || command -v python || true)" if [ -z "$PY" ]; then echo "verify: python not found; cannot validate JSON" >&2 exit 2 fi rc=0 for f in "${files[@]}"; do if "$PY" - "$f" <<'PYEOF' import json, sys path = sys.argv[1] try: with open(path, encoding="utf-8") as fh: data = json.load(fh) except Exception as e: print(f"FAIL {path}: not valid JSON ({e})") sys.exit(1) # Only judge files that look like n8n workflows (have a nodes key). if not isinstance(data, dict) or "nodes" not in data: print(f"skip {path}: not an n8n workflow (no top-level 'nodes')") sys.exit(0) errs = [] nodes = data.get("nodes") if not isinstance(nodes, list) or len(nodes) == 0: errs.append("`nodes` must be a non-empty array") conns = data.get("connections") if not isinstance(conns, dict): errs.append("`connections` must be an object") def is_trigger(n): t = (n.get("type") or "").lower() if isinstance(n, dict) else "" return t.endswith("webhook") or t.endswith("trigger") or t.endswith("cron") if isinstance(nodes, list) and not any(is_trigger(n) for n in nodes): errs.append("no trigger node found (type ending in webhook/trigger/cron)") if errs: print(f"FAIL {path}: " + "; ".join(errs)) sys.exit(1) print(f"ok {path}: {len(nodes)} nodes, trigger present, connections object") sys.exit(0) PYEOF then :; else rc=1; fi done exit "$rc"
-
-
SKILL.md 11.3 KB
--- name: automation-flows description: "Use when building or fixing a no-code automation on n8n, Make, or Zapier — trigger to multi-app steps with data mapping, dedup, retries and an error path — or picking the platform by billing unit (task vs credit vs execution). NOT a typed API client in code (that is api-connector-builder), NOT a webhook receiver in your own app (that is webhooks)." tags: [automation, n8n, make, zapier, no-code, workflows, error-handling, integrations] recommends: [automation-strategy, n8n, make, zapier, power-automate, webhooks, api-connector-builder, error-handling, stripe, notion-connector, google-workspace, whatsapp-telegram] profiles: [] origin: risco --- # Automation flows — glue many SaaS apps on a visual platform, with an error path that actually fires You are building a working automation on a hosted visual platform: a trigger, a chain of app actions with branching and explicit data mapping, and an error-handling and retry strategy without which the flow is not done. Your job is two things at once: **platform judgement** (pick n8n vs Make vs Zapier by the constraints) and a **buildable artifact** (an importable n8n workflow JSON, or a precise numbered build sheet for Make/Zapier, which have no portable export). This skill stops the moment the right answer is real code. Writing a typed API client → `../api-connector-builder/SKILL.md`. Building the endpoint that *receives* a webhook in your own app → `../webhooks/SKILL.md`. Scripting one vendor directly → `../stripe/SKILL.md`, `../notion-connector/SKILL.md`, `../google-workspace/SKILL.md`, `../whatsapp-telegram/SKILL.md`. ## 1. Pick the platform The single most expensive mistake is choosing on familiarity instead of cost model. The three platforms bill on fundamentally different units, and at volume that gap is 10×. | Constraint | Zapier | Make | n8n | | --- | --- | --- | --- | | **Billing unit** (why it dominates cost) | per **task** — every action counts | per **credit** — each module action = 1 credit (was "operations" until 2025-08-27; converted 1:1) | per **execution** — whole run = 1, any step count | | **Free tier** | 100 tasks/mo | 1,000 ops/mo | self-host free, unlimited execs | | **Entry paid** | Pro ≈ $19.99/mo (billed annually), 750 tasks | Core ≈ $9/mo, 10k credits (billing unit became **credits** on 2025-08-27) | cloud Starter ≈ €20/mo (billed annually), 2,500 execs; self-host = $0 | | **App breadth** (obscure-app signal) | ≈ 8,000+ integrations — widest | ≈ 1,500, often deeper per app | ≈ 1,000 nodes + generic HTTP node + code | | **Self-host / data residency** | no | no | yes — your infra, your data | | **Who maintains it** | non-technical-friendly | mid; visual but richer | technical; you run the box (n8n 2.0, stable Dec 2025, made isolated code execution the default — Code nodes run in sandboxed task runners) | **Worked cost example.** A 10-step flow run 10,000×/month: - Zapier: ~100,000 tasks (10 actions × 10k) → well past the Pro tier, into the high tiers. - Make: ~100,000 credits → similar pressure. - n8n: **10,000 executions** regardless of step count; self-hosted = **$0**. For complex, high-volume flows, n8n's execution model can cut cost 80–90% vs Zapier. Pricing and version figures move; re-check the **primary vendor pages** before quoting a customer (the `≈` is a hedge, not a guarantee): Zapier zapier.com/pricing, Make make.com/en/pricing, n8n n8n.io/pricing, and the n8n 2.0 release note blog.n8n.io/introducing-n8n-2-0. Make's switch to **credits** as the billing unit (2025-08-27) and n8n 2.0's **sandboxed-by-default code execution** (stable Dec 2025) are the two facts most likely to surprise someone who learned these tools a year ago. Decision in one line per row: bill on the unit that matches your shape — many short flows favor task/op platforms; few long flows favor n8n. Obscure app you can't find a node for → Zapier. Raw HTTP / custom code / data must stay on your infra → n8n. Non-technical owner who never wants to SSH → Zapier or Make cloud. ## 2. Anatomy: trigger → steps → output A flow has **exactly one trigger**. Then a chain of action steps. Map every field explicitly. **Trigger: prefer webhook/push over polling.** A webhook trigger (Zapier *Catch Hook*, Make custom webhook, n8n Webhook node) fires on an inbound POST — near-instant. A polling trigger (Zapier *Retrieve Poll*) does a periodic GET; the interval depends on plan, **1–15 minutes between checks**. Polling costs latency, costs runs (it fires even when nothing changed), and can miss events between polls. ```text Bad: Trigger = "poll Airtable for new rows every 15 min" → up to 15 min stale, burns runs on empty checks Good: Trigger = Airtable "new record" webhook → fires the instant the row lands, zero idle runs ``` **Map data explicitly. Never assume field names survive a hop.** The Typeform field `email` does not arrive at the Slack step called `email` — it arrives as a node-output reference you must wire by hand. Pin a real sample, look at the actual output keys, map from those. **Add a guard early.** Put a filter/condition right after the trigger so junk events stop before they hit an external API: drop test payloads, require the fields you need to be non-empty, exit on the wrong event type. ## 3. Error handling — the spine Every flow ships with an error path, because a flow without one is a silent failure waiting for the day the API hiccups and nobody notices the orders stopped syncing. Before you call a flow done you must be able to point at three things: where a failed run goes, how many times it retries, and who gets told. Full per-platform recipes (including the manual exponential-backoff loop) live in `references/error-handling.md`, and the retry/backoff theory under them in `../error-handling/SKILL.md`; here is the working core. **n8n.** Build a dedicated **Error Workflow** that begins with the **Error Trigger** node — it runs only when a monitored workflow fails. Wire it to Slack/email/a log row, then set it as the main flow's `settings.errorWorkflow`. On risky nodes (anything hitting an external API) toggle **Retry On Fail** (Max Tries 3–5, set a Wait between tries) and, where a single failed item shouldn't kill the run, **Continue On Fail** (the node emits an error object instead of halting). n8n's built-in retry is **linear** — for true exponential backoff you build a wait/loop yourself (recipe in references). **Make.** Attach an error handler to the risky module: - **Break** — the production default. Sends the failed run to the **Incomplete Executions** queue (no data loss) and can auto-retry from there. - **Resume** — supply a hard-coded fallback value and continue. - **Ignore** — continue past a non-critical failure. - **Commit** — end marked success. **Rollback** — end marked error and try to revert (not all modules support revert → can leave inconsistency). - Always put a **filter before** any external-API module to validate data first. **Zapier.** **Autoreplay** automatically replays failed steps, up to **5 retries** per step — but it's account-wide and turns OFF for a Zap once that Zap is published with its own custom error handling. **Filters** gate a Zap so it only proceeds when data is the right shape. **Paths** give if/then branching, including a fallback branch on error. | Concern | n8n | Make | Zapier | | --- | --- | --- | --- | | Auto-retry | Retry On Fail (Max Tries 3–5, linear) | Break → Incomplete Executions auto-retry | Autoreplay (5/step, account-wide) | | Don't halt on one bad item | Continue On Fail | Ignore / Resume | Filter to skip | | Branch / fallback | IF + Error Workflow | router + Resume | Paths | | Failure alert | Error Trigger → Slack/email | error handler → notify module | published Zap error notification | | Exponential backoff | manual wait/loop | manual | not native | ## 4. Idempotency & dedup Flows commonly **run twice for one event**: providers deliver webhooks at-least-once, and retries replay. If your flow does a non-idempotent write (create a charge, send an email, insert a row), a double-fire means a double charge or a duplicate record. Fix: **dedup on a stable key** (the event id / external id) *before* any non-idempotent action. - **n8n** — a check-before-write node or DB lookup keyed on the id; skip if seen. - **Make** — a **data store** keyed on the id; check, then write the key. - **Zapier** — a storage/lookup step (Storage by Zapier) keyed on the id; filter out if present. ```text Bad: webhook → create Notion row (Stripe retries the event → two rows) Good: webhook → lookup event_id in store → filter "not seen" → create Notion row → save event_id ``` ## 5. Test & observe before publish - Pin a real sample payload (or use the platform's test execution) — don't reason about field names blindly. - **Deliberately fire the error branch**: force a bad value, watch the failed run land where you expect. - Confirm the alert **actually arrives** — send the test Slack/email and see it in the channel, not just "it should fire". - Re-send the *same* event and confirm the dedup guard blocks the second run. - Only then publish. (On Zapier, remember publishing with custom error handling turns Autoreplay off for that Zap.) ## 6. Emit the artifact **If n8n is chosen, produce an importable workflow JSON** the user can paste into *Import from File/Clipboard*. It must have a non-empty `nodes` array (including a trigger node), a `connections` object, and `settings.errorWorkflow` pointing at the Error Workflow. Reference credentials by the n8n **credential store**, never paste secrets inline. Full schema and a minimal trigger→action→error example: `references/n8n-workflow-json.md`. **If Make or Zapier is chosen, produce a numbered build sheet** — neither has a portable export you can hand over. One row per step: `# | app | action | field mapping (source → target) | error directive`. End with the trigger type and the dedup key. Run `scripts/verify.sh` on any JSON you emit: read-only, no network, no credentials — it parses the file and checks a non-empty `nodes` array, a `connections` object, and ≥1 trigger node (exits 0 on an empty target). ## Anti-patterns | Anti-pattern | Why it bites | Do instead | | --- | --- | --- | | No error path | First API hiccup, the flow dies silently; you find out from an angry customer | Wire the platform's error handler + a real alert before shipping | | Polling when a webhook exists | 1–15 min stale, burns runs on empty checks | Use the push/webhook trigger | | 12-step branching logic crammed into Zapier | Task billing explodes; logic gets unmaintainable | Move complex/high-volume logic to n8n | | Blind field mapping | `email` ≠ the field the next step calls `email`; data silently lands empty | Pin a sample, map from real output keys | | Non-idempotent write, no dedup | At-least-once delivery → double charge / duplicate row | Dedup on event id before the write | | Secrets pasted inline in a node | Leaked in exports, unrotatable, shared everywhere | Use the platform credential store, reference by name | | One mega-flow doing everything | Unreadable, untestable, one failure nukes all | Split: trigger → sub-flow per concern | | Choosing platform by familiarity | Bill 10× higher than the right unit; "my automation bill exploded" | Pick by billing unit (task vs op vs execution) up front |
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.