Automations
A job that runs on a schedule without anyone opening a chat. Import it and switch it on.Importable n8n workflow files. No AI model anywhere in them.n8n workflow JSON. Import, set credentials, leave DRY_RUN on for the first runs.
- n8n workflow: set orders to Delivering on delivery dayAutomationn8n workflown8nfromfrom← Build Your First n8n Workflow in an AfternoonBuild Your First n8n Workflow in an Afternoon (Schedule, Fetch, Filter, Update)n8n: self-hosted runner, credential store, and a dry-run-gated schedule → HTTP → filter → HTTP workflow · Automations Without a DeveloperAutomations Without a Developer (n8n, CSV)Automation Patterns
An automation you can import: every morning it finds the orders going out today, moves them to Delivering, and posts one line to Slack.An automation you can import: every morning it finds the orders going out today, moves them to Delivering, and posts one line to Slack.n8n workflow JSON: schedule →
GET /public/v1/orders?statuses[]=READY_TO_SHIP→ sparsePOSTto DELIVERING → Slack summary. DRY_RUN is on in the Config node.A piece of itExampleExample "name": "Distru: set orders to DELIVERING on delivery day", "meta": { "description": "version 1.1 (2026-09-17). Every morning, fetch READY_TO_SHIP sales orders whose delivery_da… "instanceId": "no-bullshit-ai-course" }, "nodes": [{ "name": "Distru: set orders to DELIVERING on delivery day", "meta": { "description": "version 1.1 (2026-09-17). Every morning, fetch READY_TO_SHIP sales orders whose delivery_datetime falls today, set them to DELIVERING, post one summary line to Slack. Pure automation, no LLM. From Distru's No Bullshit AI Course (module 05, lesson 2). Leave DRY_RUN=true in the Config node for the first runs: the workflow then only reports what it would change. Endpoints, filter names and status values checked against the Distru public API order controller on 2026-09-17: GET /public/v1/orders?delivery_datetime=after,before&statuses[]=READY_TO_SHIP returns {data, next_page}; POST /public/v1/orders with {id, status} is a sparse update. DELIVERING needs every line item fulfilled and, for package-tracked items, a compliance transfer attached; orders that are not ready come back as a 400 and land in the failure branch. Credentials: an n8n Header Auth credential with name 'Authorization' and value 'Bearer YOUR_API_TOKEN' (token from Distru Settings > Distru API), plus a Slack credential. Set the workflow timezone in Settings before enabling the schedule. MIT.", "instanceId": "no-bullshit-ai-course" }, "nodes": [ { "parameters": { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 6 * * *" }] } }, "id": "n1", "name": "Every morning 06:00", "type": "n8n-nodes-base.scheduleTrigger", "typeVersion": 1.2, "position": [0, 0] }, { "parameters": { "assignments": { "assignments": [ { "id": "a1", "name": "BASE_URL", "value": "https://app.distru.com/public/v1", "type": "string" }, { "id": "a2", "name": "DRY_RUN", "value": "true", "type": "string" }, { "id": "a3", "name": "MAX_ORDERS_PER_RUN", "value": "50", "type": "string" }, { "id": "a4", "name": "DAY_START", "value": "={{ $now.startOf('day').toISO() }}", "type": "string" }, { "id": "a5", "name": "DAY_END", "value": "={{ $now.endOf('day').toISO() }}", "type": "string" }, { "id": "a6", "name": "FROM_STATUS", "value": "READY_TO_SHIP", "type": "string" }, { "id": "a7", "name": "TO_STATUS", "value": "DELIVERING", "type": "string" }, { "id": "a8", "name": "SLACK_CHANNEL", "value": "#ops", "type": "string" } ] }, "options": {} }, "id": "n2", "name": "Config", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [220, 0] }, { "parameters": { "method": "GET", "url": "={{ $('Config').item.json.BASE_URL }}/orders", "authentication": "genericCredentialType", "genericAuthType": "httpHeaderAuth", "sendQuery": true, "queryParameters": { "parameters": [ { "name": "delivery_datetime", "value": "={{ $('Config').item.json.DAY_START }},{{ $('Config').item.json.DAY_END }}" }, { "name": "statuses[]", "value": "={{ $('Config').item.json.FROM_STATUS }}" } ] }, "options": { "response": { "response": { "fullResponse": false } } } }, "id": "n3", "name": "GET today's READY_TO_SHIP orders", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [440, 0], "retryOnFail": true, "maxTries": 3, "waitBetweenTries": 5000, "credentials": { "httpHeaderAuth": { "id": "REPLACE_ME", "name": "Distru API (Authorization: Bearer ...)" } } }, { "parameters": { "jsCode": "// GET /orders returns { data: [...], next_page: url|null }. One n8n item per order.\n// If next_page is set there were more orders than one page; we stop and say so rather than guess.\nconst body = $input.first().json;\nconst orders = Array.isArray(body) ? body : (body.data ?? []);\nconst truncated = Boolean(body.next_page);\nreturn orders.map(o => ({ json: { ...o, _truncated: truncated } }));" }, "id": "n4", "name": "One item per order", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [660, 0] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict" }, "conditions": [ { "id": "c1", "leftValue": "={{ $json.status }}", "rightValue": "={{ $('Config').item.json.FROM_STATUS }}", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "id": "n5", "name": "Still READY_TO_SHIP (belt and braces)", "type": "n8n-nodes-base.filter", "typeVersion": 2, "position": [880, 0] }, { "parameters": { "maxItems": "={{ Number($('Config').item.json.MAX_ORDERS_PER_RUN) }}", "keep": "firstItems" }, "id": "n6", "name": "Cap per run", "type": "n8n-nodes-base.limit", "typeVersion": 1, "position": [1100, 0] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict" }, "conditions": [ { "id": "d1", "leftValue": "={{ $('Config').item.json.DRY_RUN }}", "rightValue": "true", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "id": "n7", "name": "Dry run?", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [1320, 0] }, { "parameters": { "method": "POST", "url": "={{ $('Config').item.json.BASE_URL }}/orders", "authentication": "genericCredentialType", "genericAuthType": "httpHeaderAuth", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ id: $json.id, status: $('Config').item.json.TO_STATUS }) }}", "options": { "batching": { "batch": { "batchSize": 1, "batchInterval": 500 } } } }, "id": "n8", "name": "POST status = DELIVERING", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [1540, 120], "credentials": { "httpHeaderAuth": { "id": "REPLACE_ME", "name": "Distru API (Authorization: Bearer ...)" } }, "onError": "continueErrorOutput" }, { "parameters": { "jsCode": "// One summary line. Works for both the dry-run and the live branch.\nconst items = $input.all();\nconst cfg = $('Config').first().json;\nconst dry = cfg.DRY_RUN === 'true';\nconst ids = items.map(i => i.json.order_number ?? i.json.id).join(', ');\nconst truncated = items.some(i => i.json._truncated);\nlet text = dry\n ? `DRY RUN: would set ${items.length} order(s) to ${cfg.TO_STATUS} today: ${ids || 'none'}`\n : `Set ${items.length} order(s) to ${cfg.TO_STATUS} today: ${ids || 'none'}`;\nif (truncated) text += `\\nNOTE: more than one page of orders matched; only the first page was handled. Run again or follow next_page.`;\nreturn [{ json: { text } }];" }, "id": "n9", "name": "Summary line", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [1760, 0] }, { "parameters": { "select": "channel", "channelId": { "__rl": true, "mode": "name", "value": "={{ $('Config').first().json.SLACK_CHANNEL }}" }, "text": "={{ $json.text }}", "otherOptions": {} }, "id": "n10", "name": "Slack #ops", "type": "n8n-nodes-base.slack", "typeVersion": 2.2, "position": [1980, 0], "credentials": { "slackApi": { "id": "REPLACE_ME", "name": "Slack" } } }, { "parameters": { "jsCode": "// Failed writes land here. A 400 usually means the order is not ready for DELIVERING\n// (unfulfilled line item, no customer, or no compliance transfer attached). Post them so a human sees them.\nconst failed = $input.all().map(i => {\n const o = i.json;\n const msg = o.error?.message ?? (Array.isArray(o.errors) ? o.errors.map(e => e.message).join('; ') : JSON.stringify(o.error ?? o));\n return `${o.order_number ?? o.id ?? '?'}: ${msg}`;\n});\nreturn [{ json: { text: `FAILED to update ${failed.length} order(s):\\n` + failed.join('\\n') } }];" }, "id": "n11", "name": "Failures → text", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [1760, 260] } ], "connections": { "Every morning 06:00": { "main": [[{ "node": "Config", "type": "main", "index": 0 }]] }, "Config": { "main": [[{ "node": "GET today's READY_TO_SHIP orders", "type": "main", "index": 0 }]] }, "GET today's READY_TO_SHIP orders": { "main": [[{ "node": "One item per order", "type": "main", "index": 0 }]] }, "One item per order": { "main": [[{ "node": "Still READY_TO_SHIP (belt and braces)", "type": "main", "index": 0 }]] }, "Still READY_TO_SHIP (belt and braces)": { "main": [[{ "node": "Cap per run", "type": "main", "index": 0 }]] }, "Cap per run": { "main": [[{ "node": "Dry run?", "type": "main", "index": 0 }]] }, "Dry run?": { "main": [ [{ "node": "Summary line", "type": "main", "index": 0 }], [{ "node": "POST status = DELIVERING", "type": "main", "index": 0 }] ] }, "POST status = DELIVERING": { "main": [ [{ "node": "Summary line", "type": "main", "index": 0 }], [{ "node": "Failures → text", "type": "main", "index": 0 }] ] }, "Summary line": { "main": [[{ "node": "Slack #ops", "type": "main", "index": 0 }]] }, "Failures → text": { "main": [[{ "node": "Slack #ops", "type": "main", "index": 0 }]] } }, "settings": { "executionOrder": "v1", "timezone": "America/Los_Angeles" }, "pinData": {}, "tags": [{ "name": "distru" }, { "name": "no-bullshit-ai-course" }] } - n8n workflow: Distru deliveries to Google CalendarAutomationn8n workflown8nfromfrom← Deliveries on Your Google Calendar, AutomaticallyDeliveries on Your Google Calendar, Automatically (One Event per Order, Keyed on the Order ID)Calendar sync and notifications: event id = order id, find-before-write, UTC to local, Slack or SMS digest · Automations Without a DeveloperAutomations Without a Developer (n8n, CSV)Automation Patterns
An automation you can import: every order with a delivery date becomes a calendar event, and it never makes a second event for the same order.An automation you can import: every order with a delivery date becomes a calendar event, and it never makes a second event for the same order.n8n workflow JSON: Distru orders → Google Calendar events keyed on the order UUID, so re-runs update instead of duplicating.
A piece of itExampleExample "name": "Distru: deliveries onto Google Calendar (idempotent)", "meta": { "description": "version 1.0 (2026-09-17). Every 15 minutes, fetch sales orders with a delivery_datetime in… "instanceId": "no-bullshit-ai-course" }, "nodes": [{ "name": "Distru: deliveries onto Google Calendar (idempotent)", "meta": { "description": "version 1.0 (2026-09-17). Every 15 minutes, fetch sales orders with a delivery_datetime in the next N days and mirror each one as a Google Calendar event. One event per order, forever: the calendar event id is derived from the Distru order id ('distru' + the id without dashes, which fits Google's a-v/0-9 id alphabet), so the workflow looks the event up by id before it creates, updates when the date or total moved, and deletes when the order is CANCELED. Re-running never duplicates. Pure automation, no LLM. From Distru's No Bullshit AI Course (module 05, lesson 3). Credentials: n8n Header Auth for Distru (name 'Authorization', value 'Bearer YOUR_API_TOKEN'), a Google Calendar OAuth2 credential, Slack. Set TZ in the Config node to the calendar's time zone; Distru returns delivery_datetime in UTC. Leave DRY_RUN=true until the Slack plan looks right. Endpoint facts checked against the Distru order controller and Google Calendar API reference on 2026-09-17; n8n Google Calendar node parameters checked against the node source (typeVersion 1.3). MIT.", "instanceId": "no-bullshit-ai-course" }, "nodes": [ { "parameters": { "rule": { "interval": [{ "field": "minutes", "minutesInterval": 15 }] } }, "id": "c1", "name": "Every 15 minutes", "type": "n8n-nodes-base.scheduleTrigger", "typeVersion": 1.2, "position": [0, 0] }, { "parameters": { "assignments": { "assignments": [ { "id": "a1", "name": "BASE_URL", "value": "https://app.distru.com/public/v1", "type": "string" }, { "id": "a2", "name": "CALENDAR_ID", "value": "primary", "type": "string" }, { "id": "a3", "name": "TZ", "value": "America/Los_Angeles", "type": "string" }, { "id": "a4", "name": "DAYS_AHEAD", "value": "14", "type": "string" }, { "id": "a5", "name": "EVENT_MINUTES", "value": "60", "type": "string" }, { "id": "a6", "name": "DRY_RUN", "value": "true", "type": "string" }, { "id": "a7", "name": "SLACK_CHANNEL", "value": "#ops", "type": "string" }, { "id": "a8", "name": "WINDOW_START", "value": "={{ $now.startOf('day').toISO() }}", "type": "string" }, { "id": "a9", "name": "WINDOW_END", "value": "={{ $now.plus({ days: 14 }).endOf('day').toISO() }}", "type": "string" } ] }, "options": {} }, "id": "c2", "name": "Config", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [220, 0] }, { "parameters": { "method": "GET", "url": "={{ $('Config').item.json.BASE_URL }}/orders", "authentication": "genericCredentialType", "genericAuthType": "httpHeaderAuth", "sendQuery": true, "queryParameters": { "parameters": [ { "name": "delivery_datetime", "value": "={{ $('Config').item.json.WINDOW_START }},{{ $('Config').item.json.WINDOW_END }}" } ] }, "options": {} }, "id": "c3", "name": "GET orders delivering in window", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [440, 0], "retryOnFail": true, "maxTries": 3, "waitBetweenTries": 5000, "credentials": { "httpHeaderAuth": { "id": "REPLACE_ME", "name": "Distru API (Authorization: Bearer ...)" } } }, { "parameters": { "jsCode": "// One item per order, with everything the calendar needs already computed.\n// Google event ids may only use a-v and 0-9 (5..1024 chars). A UUID without dashes is 0-9a-f, so it fits.\nconst cfg = $('Config').first().json;\nconst body = $input.first().json;\nconst orders = Array.isArray(body) ? body : (body.data ?? []);\nconst truncated = Boolean(body.next_page);\nconst minutes = Number(cfg.EVENT_MINUTES) || 60;\n\nreturn orders\n .filter(o => o.delivery_datetime)\n .map(o => {\n const start = DateTime.fromISO(o.delivery_datetime, { zone: 'utc' }).setZone(cfg.TZ);\n const end = start.plus({ minutes });\n const company = o.company?.name ?? 'Unknown customer';\n const eventId = 'distru' + String(o.id).replace(/-/g, '').toLowerCase();\n return { json: {\n orderId: o.id,\n orderNumber: o.order_number,\n status: o.status,\n eventId,\n start: start.toISO(),\n end: end.toISO(),\n summary: `Delivery ${o.order_number} · ${company}`,\n description: [\n `Distru order ${o.order_number}`,\n `Customer: ${company}`,\n `Status: ${o.status}`,\n `Total: ${o.total}`,\n `Payment terms: ${o.payment_term_name ?? 'n/a'}`,\n `Synced by n8n; do not edit here, edit the order in Distru.`\n ].join('\\n'),\n location: o.shipping_location?.address ?? '',\n wantDelete: o.status === 'CANCELED',\n _truncated: truncated\n } };\n });" }, "id": "c4", "name": "Prepare events", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [660, 0] }, { "parameters": { "resource": "event", "operation": "get", "calendar": { "__rl": true, "mode": "id", "value": "={{ $('Config').item.json.CALENDAR_ID }}" }, "eventId": "={{ $json.eventId }}", "options": {} }, "id": "c5", "name": "Find existing event by id", "type": "n8n-nodes-base.googleCalendar", "typeVersion": 1.3, "position": [880, 0], "onError": "continueErrorOutput", "credentials": { "googleCalendarOAuth2Api": { "id": "REPLACE_ME", "name": "Google Calendar" } } }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "// Decide what to do with this order. Runs once per item, on both outputs of the lookup:\n// output 0 = the event exists (json is the Google event), output 1 = lookup failed (404 = not there yet).\nconst order = $('Prepare events').item.json;\nconst found = !$json.error && Boolean($json.id);\nconst notFound = Boolean($json.error) && /404|not\\s*found/i.test(JSON.stringify($json.error));\nif ($json.error && !notFound) {\n // Auth or quota problem, not a missing event. Do not create anything on top of it.\n return { json: { ...order, action: 'error', reason: JSON.stringify($json.error).slice(0, 300) } };\n}\nlet action = 'skip';\nif (found && order.wantDelete) action = 'delete';\nelse if (found && $json.status === 'cancelled') {\n // Someone deleted the event by hand. The id is still taken, so a create would 409. Say so.\n return { json: { ...order, action: 'error', reason: 'calendar event was deleted by hand; restore it from the calendar trash or wait 30 days for the id to free up' } };\n}\nelse if (found) {\n const sameStart = ($json.start?.dateTime ?? '') !== '' && DateTime.fromISO($json.start.dateTime).toMillis() === DateTime.fromISO(order.start).toMillis();\n const sameText = $json.summary === order.summary && ($json.description ?? '') === order.description;\n action = (sameStart && sameText) ? 'skip' : 'update';\n}\nelse if (!found && !order.wantDelete) action = 'create';\nreturn { json: { ...order, action } };" }, "id": "c6", "name": "Plan: create / update / delete / skip", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [1100, 0] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict" }, "conditions": [ { "id": "f1", "leftValue": "={{ $json.action }}", "rightValue": "skip", "operator": { "type": "string", "operation": "notEquals" } } ], "combinator": "and" }, "options": {} }, "id": "c7", "name": "Something to do?", "type": "n8n-nodes-base.filter", "typeVersion": 2, "position": [1320, 0] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict" }, "conditions": [ { "id": "d1", "leftValue": "={{ $('Config').item.json.DRY_RUN }}", "rightValue": "true", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "id": "c8", "name": "Dry run?", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [1540, 0] }, { "parameters": { "rules": { "values": [ { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict" }, "conditions": [{ "id": "s1", "leftValue": "={{ $json.action }}", "rightValue": "create", "operator": { "type": "string", "operation": "equals" } }], "combinator": "and" }, "renameOutput": true, "outputKey": "create" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict" }, "conditions": [{ "id": "s2", "leftValue": "={{ $json.action }}", "rightValue": "update", "operator": { "type": "string", "operation": "equals" } }], "combinator": "and" }, "renameOutput": true, "outputKey": "update" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict" }, "conditions": [{ "id": "s3", "leftValue": "={{ $json.action }}", "rightValue": "delete", "operator": { "type": "string", "operation": "equals" } }], "combinator": "and" }, "renameOutput": true, "outputKey": "delete" } ] }, "options": { "fallbackOutput": "extra" } }, "id": "c9", "name": "Which write?", "type": "n8n-nodes-base.switch", "typeVersion": 3.2, "position": [1760, 160] }, { "parameters": { "resource": "event", "operation": "create", "calendar": { "__rl": true, "mode": "id", "value": "={{ $('Config').item.json.CALENDAR_ID }}" }, "start": "={{ $json.start }}", "end": "={{ $json.end }}", "useDefaultReminders": true, "additionalFields": { "id": "={{ $json.eventId }}", "summary": "={{ $json.summary }}", "description": "={{ $json.description }}", "location": "={{ $json.location }}" } }, "id": "c10", "name": "Create event (id = order)", "type": "n8n-nodes-base.googleCalendar", "typeVersion": 1.3, "position": [1980, 40], "onError": "continueErrorOutput", "credentials": { "googleCalendarOAuth2Api": { "id": "REPLACE_ME", "name": "Google Calendar" } } }, { "parameters": { "resource": "event", "operation": "update", "calendar": { "__rl": true, "mode": "id", "value": "={{ $('Config').item.json.CALENDAR_ID }}" }, "eventId": "={{ $json.eventId }}", "modifyTarget": "event", "useDefaultReminders": true, "updateFields": { "start": "={{ $json.start }}", "end": "={{ $json.end }}", "summary": "={{ $json.summary }}", "description": "={{ $json.description }}", "location": "={{ $json.location }}" } }, "id": "c11", "name": "Update event", "type": "n8n-nodes-base.googleCalendar", "typeVersion": 1.3, "position": [1980, 200], "onError": "continueErrorOutput", "credentials": { "googleCalendarOAuth2Api": { "id": "REPLACE_ME", "name": "Google Calendar" } } }, { "parameters": { "resource": "event", "operation": "delete", "calendar": { "__rl": true, "mode": "id", "value": "={{ $('Config').item.json.CALENDAR_ID }}" }, "eventId": "={{ $json.eventId }}", "options": {} }, "id": "c12", "name": "Delete event (order canceled)", "type": "n8n-nodes-base.googleCalendar", "typeVersion": 1.3, "position": [1980, 360], "onError": "continueErrorOutput", "credentials": { "googleCalendarOAuth2Api": { "id": "REPLACE_ME", "name": "Google Calendar" } } }, { "parameters": { "jsCode": "// One line per run so a human can read what happened. Empty runs post nothing.\nconst items = $input.all();\nif (!items.length) return [];\nconst cfg = $('Config').first().json;\nconst dry = cfg.DRY_RUN === 'true';\nconst by = { create: [], update: [], delete: [], error: [] };\nfor (const i of items) (by[i.json.action] ?? by.error).push(i.json.orderNumber ?? i.json.orderId);\nconst line = (k) => by[k].length ? `${k}: ${by[k].length} (${by[k].join(', ')})` : null;\nconst parts = ['create', 'update', 'delete', 'error'].map(line).filter(Boolean);\nlet text = `${dry ? 'DRY RUN calendar plan' : 'Calendar sync'}: ` + parts.join(' · ');\nif (items.some(i => i.json._truncated)) text += '\\nNOTE: more than one page of orders matched; only the first page was handled.';\nreturn [{ json: { text } }];" }, "id": "c13", "name": "Summary line", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [2200, 0] }, { "parameters": { "select": "channel", "channelId": { "__rl": true, "mode": "name", "value": "={{ $('Config').first().json.SLACK_CHANNEL }}" }, "text": "={{ $json.text }}", "otherOptions": {} }, "id": "c14", "name": "Slack #ops", "type": "n8n-nodes-base.slack", "typeVersion": 2.2, "position": [2420, 0], "credentials": { "slackApi": { "id": "REPLACE_ME", "name": "Slack" } } }, { "parameters": { "jsCode": "// Google refused a write. Most common: 409 on create (id already exists: a previous run made it and\n// the lookup failed), 403 rate limit / quota, 404 on update or delete (someone deleted it by hand).\nconst failed = $input.all().map(i => `${i.json.orderNumber ?? i.json.eventId ?? '?'}: ${i.json.error?.message ?? JSON.stringify(i.json.error ?? i.json).slice(0, 200)}`);\nreturn [{ json: { text: `Calendar sync FAILED on ${failed.length} event(s):\\n` + failed.join('\\n') } }];" }, "id": "c15", "name": "Failures → text", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [2200, 300] } ], "connections": { "Every 15 minutes": { "main": [[{ "node": "Config", "type": "main", "index": 0 }]] }, "Config": { "main": [[{ "node": "GET orders delivering in window", "type": "main", "index": 0 }]] }, "GET orders delivering in window": { "main": [[{ "node": "Prepare events", "type": "main", "index": 0 }]] }, "Prepare events": { "main": [[{ "node": "Find existing event by id", "type": "main", "index": 0 }]] }, "Find existing event by id": { "main": [ [{ "node": "Plan: create / update / delete / skip", "type": "main", "index": 0 }], [{ "node": "Plan: create / update / delete / skip", "type": "main", "index": 0 }] ] }, "Plan: create / update / delete / skip": { "main": [[{ "node": "Something to do?", "type": "main", "index": 0 }]] }, "Something to do?": { "main": [[{ "node": "Dry run?", "type": "main", "index": 0 }]] }, "Dry run?": { "main": [ [{ "node": "Summary line", "type": "main", "index": 0 }], [{ "node": "Which write?", "type": "main", "index": 0 }] ] }, "Which write?": { "main": [ [{ "node": "Create event (id = order)", "type": "main", "index": 0 }], [{ "node": "Update event", "type": "main", "index": 0 }], [{ "node": "Delete event (order canceled)", "type": "main", "index": 0 }], [{ "node": "Summary line", "type": "main", "index": 0 }] ] }, "Create event (id = order)": { "main": [ [{ "node": "Summary line", "type": "main", "index": 0 }], [{ "node": "Failures → text", "type": "main", "index": 0 }] ] }, "Update event": { "main": [ [{ "node": "Summary line", "type": "main", "index": 0 }], [{ "node": "Failures → text", "type": "main", "index": 0 }] ] }, "Delete event (order canceled)": { "main": [ [{ "node": "Summary line", "type": "main", "index": 0 }], [{ "node": "Failures → text", "type": "main", "index": 0 }] ] }, "Summary line": { "main": [[{ "node": "Slack #ops", "type": "main", "index": 0 }]] }, "Failures → text": { "main": [[{ "node": "Slack #ops", "type": "main", "index": 0 }]] } }, "settings": { "executionOrder": "v1", "timezone": "America/Los_Angeles" }, "pinData": {}, "tags": [{ "name": "distru" }, { "name": "no-bullshit-ai-course" }] }