@awesomate/hosting-mcp 0.2.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +120 -4
- package/package.json +2 -2
- package/skill/awesomate-hosting/SKILL.md +9 -0
- package/skill/awesomate-hosting/scripts/bootstrap.mjs +45 -14
- package/skill/awesomate-n8n/SKILL.md +157 -0
- package/skill/awesomate-n8n/references/node-recipes.md +87 -0
- package/skill/awesomate-n8n/references/wp-form-handler.md +149 -0
package/dist/index.js
CHANGED
|
@@ -21106,6 +21106,12 @@ var StdioServerTransport = class {
|
|
|
21106
21106
|
}
|
|
21107
21107
|
};
|
|
21108
21108
|
|
|
21109
|
+
// src/index.ts
|
|
21110
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
21111
|
+
import { homedir as homedir2 } from "node:os";
|
|
21112
|
+
import { join as join2, dirname } from "node:path";
|
|
21113
|
+
import { fileURLToPath } from "node:url";
|
|
21114
|
+
|
|
21109
21115
|
// src/config.ts
|
|
21110
21116
|
import { readFileSync } from "node:fs";
|
|
21111
21117
|
import { homedir } from "node:os";
|
|
@@ -21208,12 +21214,40 @@ function hubGet(config3, path) {
|
|
|
21208
21214
|
function hubPost(config3, path, jsonBody = {}) {
|
|
21209
21215
|
return hubRequest(config3, "POST", path, jsonBody);
|
|
21210
21216
|
}
|
|
21217
|
+
function hubDelete(config3, path) {
|
|
21218
|
+
return hubRequest(config3, "DELETE", path);
|
|
21219
|
+
}
|
|
21211
21220
|
|
|
21212
21221
|
// src/index.ts
|
|
21213
21222
|
var config2;
|
|
21223
|
+
var SERVER_VERSION = (() => {
|
|
21224
|
+
try {
|
|
21225
|
+
const pkg = JSON.parse(
|
|
21226
|
+
readFileSync2(join2(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")
|
|
21227
|
+
);
|
|
21228
|
+
return pkg.version ?? "0.0.0";
|
|
21229
|
+
} catch {
|
|
21230
|
+
return "0.0.0";
|
|
21231
|
+
}
|
|
21232
|
+
})();
|
|
21233
|
+
function skillUpdateInfo() {
|
|
21234
|
+
let installedSkillVersion = null;
|
|
21235
|
+
try {
|
|
21236
|
+
installedSkillVersion = readFileSync2(
|
|
21237
|
+
join2(homedir2(), ".claude", "skills", "awesomate-hosting", ".installed-version"),
|
|
21238
|
+
"utf8"
|
|
21239
|
+
).trim() || null;
|
|
21240
|
+
} catch {
|
|
21241
|
+
}
|
|
21242
|
+
return {
|
|
21243
|
+
serverVersion: SERVER_VERSION,
|
|
21244
|
+
installedSkillVersion,
|
|
21245
|
+
updateAvailable: installedSkillVersion !== SERVER_VERSION
|
|
21246
|
+
};
|
|
21247
|
+
}
|
|
21214
21248
|
var server = new McpServer({
|
|
21215
21249
|
name: "awesomate-hosting",
|
|
21216
|
-
version:
|
|
21250
|
+
version: SERVER_VERSION
|
|
21217
21251
|
});
|
|
21218
21252
|
function textResult(data) {
|
|
21219
21253
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
@@ -21231,10 +21265,92 @@ function readTool(name, description, path) {
|
|
|
21231
21265
|
}
|
|
21232
21266
|
});
|
|
21233
21267
|
}
|
|
21234
|
-
|
|
21268
|
+
server.registerTool(
|
|
21235
21269
|
"awesomate_get_context",
|
|
21236
|
-
|
|
21237
|
-
|
|
21270
|
+
{
|
|
21271
|
+
description: "Call this FIRST each session. Returns the connected Awesomate account: plan, capabilities (shell access), plan limits, scopes this token can exercise, token expiry (warn the user if within 7 days), cPanel routing when provisioned, and skill.updateAvailable \u2014 if true, the local awesomate-hosting skill files are older than this server; tell the user to re-run Connect Claude Code from hub.awesomate.ai/sites to refresh them (this also renews the token).",
|
|
21272
|
+
inputSchema: {}
|
|
21273
|
+
},
|
|
21274
|
+
async () => {
|
|
21275
|
+
try {
|
|
21276
|
+
const ctx = await hubGet(config2, "/api/hosting-access/context");
|
|
21277
|
+
return textResult({ ...ctx, skill: skillUpdateInfo() });
|
|
21278
|
+
} catch (err) {
|
|
21279
|
+
return errorResult(err);
|
|
21280
|
+
}
|
|
21281
|
+
}
|
|
21282
|
+
);
|
|
21283
|
+
readTool(
|
|
21284
|
+
"awesomate_n8n_context",
|
|
21285
|
+
"Call FIRST before any n8n work. Returns the client's n8n instance URL, plan, whether Claude Code n8n access is consented (if consented=false, send the user to settingsUrl and re-check after), builder capability (Support Plus+), instance variant, and build/test quota limits. Companion reads once consented: GET /api/my-n8n/machine/credentials (credential names/types \u2014 never secrets), /credentials/schema/:type, /variables, plus the /api/my-n8n workflows and executions endpoints.",
|
|
21286
|
+
"/api/my-n8n/machine/context"
|
|
21287
|
+
);
|
|
21288
|
+
server.registerTool(
|
|
21289
|
+
"awesomate_n8n_deploy",
|
|
21290
|
+
{
|
|
21291
|
+
description: "Workflow lifecycle writes on the client's n8n, all consent-gated and audited server-side. Actions: 'validate' (structural + n8n-mcp check of workflowJson \u2014 ALWAYS validate before create_draft), 'create_draft' (creates an INACTIVE '[CLI] ' workflow tagged awm:client-cli; returns its webhook URLs), 'activate'/'deactivate' (activation strips the [CLI] prefix; production webhooks respond only while active; agency-managed workflows are refused), 'promote' (swaps a TESTED draft into the live workflow IN PLACE \u2014 live id + webhookIds preserved so external callers keep working; pass workflowId=the LIVE id and draftId=the tested draft; the draft is archived '[promoted <date>]'; response includes operationId for rollback), 'rollback' (restore a promote's pre-swap state \u2014 pass operationId), 'delete_draft' (inactive self-built drafts only). Get explicit user approval before activate, promote, rollback, and delete_draft. 429 quota_exceeded = daily plan limit; 403 consent_required \u2192 send user to settingsUrl.",
|
|
21292
|
+
inputSchema: {
|
|
21293
|
+
action: external_exports.enum(["validate", "create_draft", "activate", "deactivate", "promote", "rollback", "delete_draft"]),
|
|
21294
|
+
workflowJson: external_exports.record(external_exports.unknown()).optional().describe("For validate: the full workflow JSON. For create_draft: must contain name, nodes, connections (settings optional)."),
|
|
21295
|
+
workflowId: external_exports.string().optional().describe("Required for activate/deactivate/delete_draft; for promote this is the LIVE workflow id"),
|
|
21296
|
+
draftId: external_exports.string().optional().describe("promote only: the tested draft to swap into the live workflow"),
|
|
21297
|
+
operationId: external_exports.string().optional().describe("rollback only: the operationId returned by promote")
|
|
21298
|
+
}
|
|
21299
|
+
},
|
|
21300
|
+
async ({ action, workflowJson, workflowId, draftId, operationId }) => {
|
|
21301
|
+
try {
|
|
21302
|
+
if (action === "validate") {
|
|
21303
|
+
return textResult(await hubPost(config2, "/api/my-n8n/machine/workflows/validate", { workflow: workflowJson }));
|
|
21304
|
+
}
|
|
21305
|
+
if (action === "create_draft") {
|
|
21306
|
+
return textResult(await hubPost(config2, "/api/my-n8n/machine/workflows/draft", workflowJson ?? {}));
|
|
21307
|
+
}
|
|
21308
|
+
if (action === "rollback") {
|
|
21309
|
+
if (!operationId) return errorResult(new Error("operationId is required for rollback"));
|
|
21310
|
+
return textResult(
|
|
21311
|
+
await hubPost(config2, `/api/my-n8n/machine/operations/${encodeURIComponent(operationId)}/rollback`, {})
|
|
21312
|
+
);
|
|
21313
|
+
}
|
|
21314
|
+
if (!workflowId) return errorResult(new Error(`workflowId is required for ${action}`));
|
|
21315
|
+
if (action === "activate" || action === "deactivate") {
|
|
21316
|
+
return textResult(
|
|
21317
|
+
await hubPost(config2, `/api/my-n8n/machine/workflows/${encodeURIComponent(workflowId)}/activate`, {
|
|
21318
|
+
active: action === "activate"
|
|
21319
|
+
})
|
|
21320
|
+
);
|
|
21321
|
+
}
|
|
21322
|
+
if (action === "promote") {
|
|
21323
|
+
if (!draftId) return errorResult(new Error("draftId is required for promote"));
|
|
21324
|
+
return textResult(
|
|
21325
|
+
await hubPost(config2, `/api/my-n8n/machine/workflows/${encodeURIComponent(workflowId)}/promote`, { draftId })
|
|
21326
|
+
);
|
|
21327
|
+
}
|
|
21328
|
+
return textResult(
|
|
21329
|
+
await hubDelete(config2, `/api/my-n8n/machine/workflows/${encodeURIComponent(workflowId)}`)
|
|
21330
|
+
);
|
|
21331
|
+
} catch (err) {
|
|
21332
|
+
return errorResult(err);
|
|
21333
|
+
}
|
|
21334
|
+
}
|
|
21335
|
+
);
|
|
21336
|
+
server.registerTool(
|
|
21337
|
+
"awesomate_n8n_test",
|
|
21338
|
+
{
|
|
21339
|
+
description: "Fire a synthetic test payload at an ACTIVE self-built workflow's production webhook and get back the webhook response plus the executionId it caused (then poll GET /api/my-n8n/executions/:id for node-by-node results). Sends the X-Awesomate-Test header. Real side effects DO run (emails actually send) \u2014 warn the user, use their own address, prefix subjects with [TEST]. Only works on awm:client-cli-tagged workflows.",
|
|
21340
|
+
inputSchema: {
|
|
21341
|
+
workflowId: external_exports.string(),
|
|
21342
|
+
payload: external_exports.record(external_exports.unknown()).optional().describe("JSON body to POST to the webhook (format-valid, obviously-fake values)")
|
|
21343
|
+
}
|
|
21344
|
+
},
|
|
21345
|
+
async ({ workflowId, payload }) => {
|
|
21346
|
+
try {
|
|
21347
|
+
return textResult(
|
|
21348
|
+
await hubPost(config2, `/api/my-n8n/machine/workflows/${encodeURIComponent(workflowId)}/test-fire`, { payload })
|
|
21349
|
+
);
|
|
21350
|
+
} catch (err) {
|
|
21351
|
+
return errorResult(err);
|
|
21352
|
+
}
|
|
21353
|
+
}
|
|
21238
21354
|
);
|
|
21239
21355
|
readTool(
|
|
21240
21356
|
"awesomate_get_hosting_status",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awesomate/hosting-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Awesomate
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Awesomate MCP server — lets Claude manage your Awesomate WordPress hosting, plan, limits, and n8n automations",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"publishConfig": {
|
|
@@ -32,11 +32,20 @@ context note three things and cache them for the session:
|
|
|
32
32
|
- **`patExpiresAt`** — if it's within ~7 days, tell the user to open
|
|
33
33
|
**hub.awesomate.ai/sites → Connect Claude Code** and re-run the setup prompt to
|
|
34
34
|
refresh the token. If any tool returns a 401, do the same.
|
|
35
|
+
- **`skill.updateAvailable`** — if true, this skill's local files are older than
|
|
36
|
+
the MCP server. Mention it once (don't nag): re-running Connect Claude Code
|
|
37
|
+
from **hub.awesomate.ai/sites** refreshes the skill and renews the token in
|
|
38
|
+
one go. Not urgent — finish the user's actual request first.
|
|
35
39
|
- **`cpanel`** routing (host/user), present once hosting is provisioned.
|
|
36
40
|
|
|
37
41
|
If `awesomate_get_context` fails with a connectivity error, it's the user's
|
|
38
42
|
network or the API base — not an auth problem; say so.
|
|
39
43
|
|
|
44
|
+
If this skill is loaded but **no `awesomate_*` tools exist in the session at
|
|
45
|
+
all**, the MCP server was registered after Claude Code started (the bootstrap
|
|
46
|
+
just ran). Don't investigate settings files or reinstall anything — ask the
|
|
47
|
+
user to restart Claude Code and try again.
|
|
48
|
+
|
|
40
49
|
## 1. The plan model (so your nudges are accurate)
|
|
41
50
|
|
|
42
51
|
| Plan | WP sites | Custom domains | Shell / Claude Code | Notes |
|
|
@@ -68,23 +68,54 @@ function writeCreds(creds) {
|
|
|
68
68
|
* failure here never aborts the rest of setup.
|
|
69
69
|
*/
|
|
70
70
|
function installSkill() {
|
|
71
|
-
// bootstrap.mjs lives at <skill>/scripts/bootstrap.mjs → the skill dir is
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
// bootstrap.mjs lives at <skill>/scripts/bootstrap.mjs → the skill dir is
|
|
72
|
+
// `..` and every sibling of it under the package's skill/ dir is another
|
|
73
|
+
// bundled skill (e.g. awesomate-n8n). Install them all. When bootstrap is
|
|
74
|
+
// re-run from an already-installed copy (~/.claude/skills/...), the parent
|
|
75
|
+
// is the user's whole skills folder — in that case only reinstall this one
|
|
76
|
+
// skill, never its unrelated siblings.
|
|
77
|
+
const hostingSkill = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
78
|
+
const skillRoot = dirname(hostingSkill);
|
|
79
|
+
// Only the package layout (skill/<name>/) has bundled siblings; from an
|
|
80
|
+
// installed copy the parent is the user's skills folder full of unrelated
|
|
81
|
+
// skills, so fall back to just this skill.
|
|
82
|
+
const bundled = skillRoot.endsWith('/skill')
|
|
83
|
+
? readdirSync(skillRoot)
|
|
84
|
+
: [hostingSkill.split('/').pop()];
|
|
85
|
+
// Package version, stamped into each installed skill as .installed-version.
|
|
86
|
+
// The MCP server (always latest via unpinned npx) compares it against its own
|
|
87
|
+
// version and reports skill.updateAvailable in awesomate_get_context.
|
|
88
|
+
let pkgVersion = null;
|
|
89
|
+
if (skillRoot.endsWith('/skill')) {
|
|
90
|
+
try {
|
|
91
|
+
pkgVersion = JSON.parse(readFileSync(join(dirname(skillRoot), 'package.json'), 'utf8')).version ?? null;
|
|
92
|
+
} catch { /* non-fatal — marker just won't be written */ }
|
|
76
93
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
94
|
+
let installed = 0;
|
|
95
|
+
for (const name of bundled) {
|
|
96
|
+
const src = join(skillRoot, name);
|
|
97
|
+
if (!existsSync(join(src, 'SKILL.md'))) continue;
|
|
98
|
+
const dest = join(homedir(), '.claude', 'skills', name);
|
|
99
|
+
if (src === dest) { installed += 1; continue; } // already running from the installed copy
|
|
100
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
101
|
+
cpSync(src, dest, { recursive: true, force: true });
|
|
102
|
+
// Make the shell/mjs helpers executable (npm can publish them non-+x).
|
|
103
|
+
const scriptsDir = join(dest, 'scripts');
|
|
104
|
+
if (existsSync(scriptsDir)) {
|
|
105
|
+
for (const f of readdirSync(scriptsDir)) {
|
|
106
|
+
if (/\.(sh|mjs)$/.test(f)) { try { chmodSync(join(scriptsDir, f), 0o755); } catch { /* non-fatal */ } }
|
|
107
|
+
}
|
|
85
108
|
}
|
|
109
|
+
if (pkgVersion) {
|
|
110
|
+
try { writeFileSync(join(dest, '.installed-version'), `${pkgVersion}\n`); } catch { /* non-fatal */ }
|
|
111
|
+
}
|
|
112
|
+
console.log(`✓ Installed the "${name}" skill to ${dest}.`);
|
|
113
|
+
installed += 1;
|
|
114
|
+
}
|
|
115
|
+
if (installed === 0) {
|
|
116
|
+
console.log('• Skill files not found next to bootstrap — skipping skill install (MCP still works).');
|
|
117
|
+
return false;
|
|
86
118
|
}
|
|
87
|
-
console.log(`✓ Installed the "awesomate-hosting" skill to ${dest}.`);
|
|
88
119
|
return true;
|
|
89
120
|
}
|
|
90
121
|
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: awesomate-n8n
|
|
3
|
+
description: Build, test, and manage the user's Awesomate-hosted n8n automations from Claude Code. Use when the user mentions their n8n instance, workflows, automations, executions, webhooks, workflow errors, "{slug}.awesomate.io", or asks to build/change an automation or explain why one failed. Companion to the awesomate-hosting skill — same connection, same PAT.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Awesomate n8n — your automations from Claude Code
|
|
7
|
+
|
|
8
|
+
The user's business runs n8n workflows on an Awesomate-hosted instance. This
|
|
9
|
+
skill lets you read, explain, diagnose, **build, test, and activate** those
|
|
10
|
+
workflows through the Awesomate hub. Their n8n API key never reaches this
|
|
11
|
+
machine; every write is consent-gated, quota-limited, and audited on
|
|
12
|
+
Awesomate's side.
|
|
13
|
+
|
|
14
|
+
**What you can do:** everything in the read table below, plus (Support Plus
|
|
15
|
+
and above, with consent): validate workflow JSON, create inactive `[CLI]`
|
|
16
|
+
drafts, test-fire their webhooks, activate/deactivate, **promote a tested
|
|
17
|
+
draft into a live workflow in place** (id + webhook URLs preserved), roll a
|
|
18
|
+
promote back, and delete drafts — via the `awesomate_n8n_deploy` and
|
|
19
|
+
`awesomate_n8n_test` tools.
|
|
20
|
+
|
|
21
|
+
## 0. First run (every session)
|
|
22
|
+
|
|
23
|
+
Call `awesomate_n8n_context` once before any n8n work and cache the result:
|
|
24
|
+
|
|
25
|
+
- `consented: false` → give the user the `settingsUrl` link (Settings →
|
|
26
|
+
Privacy → "Allow Claude Code to Build n8n Workflows"), wait for them to
|
|
27
|
+
confirm, then call context again.
|
|
28
|
+
- `403 missingScopes` → their token predates n8n support or their plan lacks
|
|
29
|
+
it — reconnect from hub.awesomate.ai/sites (Connect Claude Code card).
|
|
30
|
+
- `capabilities.builder: false` → reads only; building is Support Plus+.
|
|
31
|
+
- `capabilities.variant` → `FFMPEG` variants include the media community
|
|
32
|
+
nodes (localFiles, better-ffmpeg); Standard does not. Never assume a node
|
|
33
|
+
exists on the target — verify in existing workflows or ask.
|
|
34
|
+
- `limits` → your daily build/test quotas and the active-workflow cap;
|
|
35
|
+
respect them, don't burn tests.
|
|
36
|
+
|
|
37
|
+
Read [references/node-recipes.md](references/node-recipes.md) BEFORE
|
|
38
|
+
designing or diagnosing — it's the live truth for this platform ($vars not
|
|
39
|
+
$env, `$json.body`, task-runner limits, activation semantics).
|
|
40
|
+
|
|
41
|
+
## 1. Reads
|
|
42
|
+
|
|
43
|
+
PAT as `Authorization: Bearer <pat>` (from `~/.awesomate/credentials.json`)
|
|
44
|
+
against `apiBase`. Never echo the PAT into the conversation.
|
|
45
|
+
|
|
46
|
+
| What | Endpoint |
|
|
47
|
+
|---|---|
|
|
48
|
+
| Session context (tool) | `awesomate_n8n_context` |
|
|
49
|
+
| List workflows | `GET /api/my-n8n/workflows` |
|
|
50
|
+
| One workflow (full JSON) | `GET /api/my-n8n/workflows/:id` |
|
|
51
|
+
| Executions | `GET /api/my-n8n/workflows/:id/executions` · detail: `GET /api/my-n8n/executions/:execId` |
|
|
52
|
+
| Credential inventory (names/types only) | `GET /api/my-n8n/machine/credentials` |
|
|
53
|
+
| Credential type schema | `GET /api/my-n8n/machine/credentials/schema/:type` |
|
|
54
|
+
| `$vars` keys | `GET /api/my-n8n/machine/variables` |
|
|
55
|
+
|
|
56
|
+
## 2. The build loop (follow ALL six phases)
|
|
57
|
+
|
|
58
|
+
**Phase 1 — Discovery.** Understand inputs/outputs, trigger, success
|
|
59
|
+
criteria, error handling. Check the credential inventory and `$vars` FIRST —
|
|
60
|
+
reuse what exists. Email decision tree: existing Gmail/Google credential in
|
|
61
|
+
inventory → reference its ID · none → offer an API-key credential (SMTP /
|
|
62
|
+
Resend / SendGrid — instant) OR guided Gmail OAuth: the user connects it in
|
|
63
|
+
their n8n UI at `{instanceUrl}/home/credentials`, you re-check the inventory
|
|
64
|
+
to verify before continuing. Never claim a credential works because it
|
|
65
|
+
exists — the inventory shows names, not validity.
|
|
66
|
+
|
|
67
|
+
**Phase 2 — Design.** Propose the architecture as an arrow diagram
|
|
68
|
+
(`Webhook -> Validate -> Send Email -> Respond`). Reasoning/execution
|
|
69
|
+
separation; descriptive node names; every external call has an error plan.
|
|
70
|
+
For externally-called workflows use a **webhook trigger with an explicit
|
|
71
|
+
path** (e.g. `signup-v1`) — not a Form Trigger.
|
|
72
|
+
|
|
73
|
+
**Phase 3 — Confirmation.** Present the design and wait for explicit
|
|
74
|
+
approval. Never create anything on their instance without it.
|
|
75
|
+
|
|
76
|
+
**Phase 4 — Implementation.** Compose the workflow JSON, then
|
|
77
|
+
`awesomate_n8n_deploy {action:'validate', workflowJson}` and fix every
|
|
78
|
+
error-severity issue before creating. Then
|
|
79
|
+
`{action:'create_draft', workflowJson:{name, nodes, connections}}` — the hub
|
|
80
|
+
prefixes `[CLI] `, tags `awm:client-cli`, pre-assigns webhookIds, applies
|
|
81
|
+
the required workflow settings, and returns the production webhook URLs.
|
|
82
|
+
|
|
83
|
+
**Phase 5 — Testing (mandatory — validation alone is never done).**
|
|
84
|
+
1. Get approval to go live for testing, then `{action:'activate', workflowId}`
|
|
85
|
+
(production webhooks only respond while active; activation drops the
|
|
86
|
+
`[CLI]` prefix).
|
|
87
|
+
2. `awesomate_n8n_test {workflowId, payload}` with a realistic, obviously
|
|
88
|
+
fake payload (`test+<runId>@…`, "TEST SUBMISSION"). Real side effects run:
|
|
89
|
+
emails actually send — send to the user's own address, `[TEST]` subject,
|
|
90
|
+
tell them first. For non-email side effects (CRM writes, payments): STOP
|
|
91
|
+
and ask before testing.
|
|
92
|
+
3. Fetch the returned executionId's detail and INSPECT node-by-node output —
|
|
93
|
+
a green final status is not inspection. Test at least: happy path, empty
|
|
94
|
+
payload, malformed payload. Confirm error branches actually routed.
|
|
95
|
+
4. Report: what ran, with what data, what passed, what needs the human to
|
|
96
|
+
verify (e.g. "check your inbox").
|
|
97
|
+
|
|
98
|
+
**Phase 6 — Wrap-up.** Give the user the workflow URL
|
|
99
|
+
(`{instanceUrl}/workflow/{id}`), the webhook URL, and what to do next. If
|
|
100
|
+
the test drafts aren't needed, `{action:'delete_draft'}` (deactivated only).
|
|
101
|
+
|
|
102
|
+
## 3. The upgrade loop (changing a LIVE self-built workflow)
|
|
103
|
+
|
|
104
|
+
Never edit a live workflow directly, and never "activate the copy" — the
|
|
105
|
+
copy has different webhook URLs and every external caller would break.
|
|
106
|
+
Instead:
|
|
107
|
+
|
|
108
|
+
1. Fetch the live workflow's JSON, duplicate it into a new draft
|
|
109
|
+
(`create_draft` with the modified JSON — new name, e.g. same name; the
|
|
110
|
+
hub adds `[CLI]` + fresh test webhookIds automatically).
|
|
111
|
+
2. Apply the requested changes to the draft; validate.
|
|
112
|
+
3. Activate the DRAFT and test it on its own (test) URLs —
|
|
113
|
+
`awesomate_n8n_test` — full testing policy applies.
|
|
114
|
+
4. Present the user a human-readable diff: nodes added/removed/changed,
|
|
115
|
+
credential changes, behaviour summary. **Wait for explicit approval.**
|
|
116
|
+
5. `awesomate_n8n_deploy {action:'promote', workflowId:<LIVE id>,
|
|
117
|
+
draftId:<draft id>}` — the hub snapshots both workflows, swaps the
|
|
118
|
+
draft's body into the live id (preserving its webhookIds so external
|
|
119
|
+
URLs never change), restores activation, and archives the draft as
|
|
120
|
+
`[promoted <date>]`. Save the returned `operationId`.
|
|
121
|
+
6. Verify: `awesomate_n8n_test` against the LIVE workflow — the ORIGINAL
|
|
122
|
+
URL must still respond. On any failure:
|
|
123
|
+
`{action:'rollback', operationId}` restores both workflows to their
|
|
124
|
+
pre-promote state, then investigate.
|
|
125
|
+
7. Cleanup: once the user confirms, `delete_draft` the archived
|
|
126
|
+
`[promoted …]` draft (it's deactivated and still CLI-tagged), or leave
|
|
127
|
+
it as a manual fallback if they prefer.
|
|
128
|
+
|
|
129
|
+
Promote only works between self-built (`awm:client-cli`) workflows. To
|
|
130
|
+
upgrade a workflow the user built in the n8n UI, duplicate it into a CLI
|
|
131
|
+
draft first and treat the new workflow as the live one going forward (its
|
|
132
|
+
URL will differ — coordinate the cutover with the user).
|
|
133
|
+
|
|
134
|
+
## 4. Diagnosing a failing workflow
|
|
135
|
+
|
|
136
|
+
Executions list → execution detail → which node failed and why → walk the
|
|
137
|
+
item data BACKWARDS (the cause is often an upstream mis-mapped field) →
|
|
138
|
+
check against node-recipes.md → explain in the user's terms with the exact
|
|
139
|
+
fix. Self-built workflows: fix via the upgrade loop above. Agency-managed workflows (`agency_managed` error): read
|
|
140
|
+
and explain freely, but changes go through Awesomate support.
|
|
141
|
+
|
|
142
|
+
## 5. Hard rules
|
|
143
|
+
|
|
144
|
+
- **Never ask for their n8n API key.** Unreachable = consent/scope/plan —
|
|
145
|
+
route to settings or reconnect.
|
|
146
|
+
- **Explicit approval gates:** before create_draft (design confirmation),
|
|
147
|
+
before activate, before promote, before rollback, before delete, before
|
|
148
|
+
any test with non-email side effects.
|
|
149
|
+
- **Agency-deployed workflows are read-only** (the hub enforces it; don't
|
|
150
|
+
fight the 403).
|
|
151
|
+
- **Respect quotas** (429 quota_exceeded = plan's daily cap — tell the user,
|
|
152
|
+
don't retry-loop).
|
|
153
|
+
- **WordPress forms** (via the awesomate-hosting skill): have the form POST
|
|
154
|
+
server-side from WP to the webhook (a small forwarder snippet), not from
|
|
155
|
+
the browser — no CORS, hides the webhook URL, spam-filterable. Full
|
|
156
|
+
pattern + per-builder recipes:
|
|
157
|
+
[references/wp-form-handler.md](references/wp-form-handler.md).
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Awesomate n8n platform notes — read before diagnosing or designing
|
|
2
|
+
|
|
3
|
+
The fleet runs standard n8n 2.x. n8n 2.0 changed defaults that older tutorials
|
|
4
|
+
(and model memory) get wrong — these notes are the live truth for THIS
|
|
5
|
+
platform. When something here conflicts with what you remember about n8n,
|
|
6
|
+
trust this file.
|
|
7
|
+
|
|
8
|
+
## Environment rules (Awesomate-specific)
|
|
9
|
+
|
|
10
|
+
1. **`$env` is blocked fleet-wide** (`N8N_BLOCK_ENV_ACCESS_IN_NODE`). `$env`
|
|
11
|
+
in a Code node OR a regular field expression throws "access to environment
|
|
12
|
+
variables is denied", and `$evaluateExpression()` is gone too. Use
|
|
13
|
+
`{{ $vars.key || 'fallback' }}` or a Set/Config node literal. The instance's
|
|
14
|
+
`$vars` keys are listed by `GET /api/my-n8n/machine/variables`.
|
|
15
|
+
2. **Task runners are on by default** (2.0+): Code nodes have no `$helpers` —
|
|
16
|
+
no `$helpers.httpRequest`, no `getBinaryDataBuffer`. HTTP calls belong in
|
|
17
|
+
HTTP Request nodes; binary work belongs in dedicated nodes.
|
|
18
|
+
3. **Media/community nodes are variant-gated.** `CUSTOM.localFiles`,
|
|
19
|
+
better-ffmpeg, and mediafx exist only on FFMPEG-variant instances
|
|
20
|
+
(Support Plus+ — `capabilities.variant` in the context tool). A workflow
|
|
21
|
+
referencing them on a Standard instance shows the node as "unknown".
|
|
22
|
+
4. **Python Code nodes have no external libraries.** JavaScript covers 95% of
|
|
23
|
+
cases; recommend it by default.
|
|
24
|
+
|
|
25
|
+
## The classics (most failures are one of these)
|
|
26
|
+
|
|
27
|
+
5. **Webhook data lives at `$json.body`**, not `$json`. A webhook workflow
|
|
28
|
+
where every downstream field is empty almost always reads
|
|
29
|
+
`{{ $json.orderId }}` instead of `{{ $json.body.orderId }}`.
|
|
30
|
+
6. **Code nodes must return `[{ json: {...} }]`** — an array of items each
|
|
31
|
+
wrapped in `json`. Returning a bare object/array is the #1 Code-node error.
|
|
32
|
+
7. **Property dependencies**: some fields only apply with their siblings set —
|
|
33
|
+
e.g. HTTP Request `sendBody: true` requires a `contentType`. When a node
|
|
34
|
+
"ignores" a configured value, look for the missing sibling switch.
|
|
35
|
+
8. **`resource` + `operation` travel together.** Partial edits that set one
|
|
36
|
+
without the other can leave the node silently misconfigured (n8n's
|
|
37
|
+
sanitizer may strip `operation`). When editing node JSON, always write both.
|
|
38
|
+
|
|
39
|
+
## Triggers & webhooks
|
|
40
|
+
|
|
41
|
+
9. **Production webhook** = `{instanceUrl}/webhook/{path}` and only responds
|
|
42
|
+
while the workflow is ACTIVE. **Test webhook** (`/webhook-test/`) only
|
|
43
|
+
works while someone is watching the editor — it is not a headless test path.
|
|
44
|
+
10. **Form Trigger ignores custom paths on 2.x** — forms serve at
|
|
45
|
+
`/form/<webhookId>`, and the webhookId regenerates when a workflow is
|
|
46
|
+
duplicated or re-imported. External forms should post to a webhook
|
|
47
|
+
trigger with an explicit `path`, not a Form Trigger.
|
|
48
|
+
11. **Activation is its own operation.** In workflow JSON the `active` field
|
|
49
|
+
is read-only; a workflow saved with `active: true` is NOT thereby active.
|
|
50
|
+
12. **Multi-trigger workflows**: a manual execution starts from the webhook
|
|
51
|
+
trigger. To exercise a schedule branch, the webhook trigger has to be
|
|
52
|
+
temporarily disabled (and re-enabled after!).
|
|
53
|
+
|
|
54
|
+
## Reading executions like an engineer
|
|
55
|
+
|
|
56
|
+
13. Success status ≠ correct behaviour. Open the execution detail and check
|
|
57
|
+
which nodes ran, what each output, and whether `onError` branches routed.
|
|
58
|
+
14. An error execution names the failing node and error — but the CAUSE is
|
|
59
|
+
often upstream (empty input from a mis-mapped field two nodes back).
|
|
60
|
+
Walk the item data backwards.
|
|
61
|
+
15. Recommended workflow settings for anything that matters:
|
|
62
|
+
`{"executionOrder": "v1", "saveExecutionProgress": true,
|
|
63
|
+
"saveDataErrorExecution": "all", "saveDataSuccessExecution": "all"}` —
|
|
64
|
+
without the save flags there may be nothing to inspect after a failure.
|
|
65
|
+
|
|
66
|
+
## Credentials
|
|
67
|
+
|
|
68
|
+
16. The n8n API never returns credential secrets, and neither does the hub —
|
|
69
|
+
the inventory is names/types/inferred purpose only.
|
|
70
|
+
17. OAuth credentials (Gmail, Google Sheets, Slack…) can only be completed in
|
|
71
|
+
the n8n browser UI — the consent screen needs a human. API-key
|
|
72
|
+
credentials (SMTP, Resend, SendGrid, HTTP header auth…) don't have that
|
|
73
|
+
constraint. This matters when advising which email/integration path is
|
|
74
|
+
fastest to set up.
|
|
75
|
+
18. "Credential could not be found" on a node usually means a sharing issue
|
|
76
|
+
(the credential exists but isn't shared with the workflow owner), not a
|
|
77
|
+
deleted credential.
|
|
78
|
+
|
|
79
|
+
## Design principles (for when you sketch workflows with the user)
|
|
80
|
+
|
|
81
|
+
- Separate reasoning from execution: AI nodes decide and route; Code/HTTP/
|
|
82
|
+
integration nodes do the work. Five chained 90%-accurate AI steps ≈ 59%.
|
|
83
|
+
- Descriptive node names ("Get Customer from CRM", never "HTTP Request 1").
|
|
84
|
+
- Every external call needs an error plan: retry, fallback, or notify — and
|
|
85
|
+
notification chains must terminate, never loop back into the workflow.
|
|
86
|
+
- One webhook path per purpose, versioned in the path if it will evolve
|
|
87
|
+
(`/webhook/signup-v1`).
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# WordPress form → n8n webhook: the forwarder pattern
|
|
2
|
+
|
|
3
|
+
The reliable way to wire a WordPress form to an n8n workflow on this
|
|
4
|
+
platform. The form NEVER posts to the n8n webhook from the browser — it
|
|
5
|
+
posts to a small server-side forwarder on the WP site, which relays to the
|
|
6
|
+
webhook.
|
|
7
|
+
|
|
8
|
+
Why the forwarder (not a direct browser POST):
|
|
9
|
+
- **No CORS** — same-origin from the browser; WP→n8n is server-to-server.
|
|
10
|
+
- **The webhook URL stays hidden** from page source and bots.
|
|
11
|
+
- **Spam filtering** (honeypot + nonce) happens before n8n ever sees a hit.
|
|
12
|
+
- **One place to update** if the webhook path ever changes.
|
|
13
|
+
|
|
14
|
+
Prerequisites: the n8n workflow exists and is ACTIVE (build it with the
|
|
15
|
+
awesomate-n8n build loop first — webhook trigger with explicit path, e.g.
|
|
16
|
+
`signup-v1`, `httpMethod: POST`, respond node returning `{"ok": true}`).
|
|
17
|
+
WP access comes from the awesomate-hosting skill (SSH + wp-cli).
|
|
18
|
+
|
|
19
|
+
## 1. The forwarder (mu-plugin — survives theme changes)
|
|
20
|
+
|
|
21
|
+
Install via SSH as `wp-content/mu-plugins/awm-form-forwarder.php`. Snapshot
|
|
22
|
+
the site first (`awesomate_snapshot_site`). Template — replace the webhook
|
|
23
|
+
URL and the expected fields:
|
|
24
|
+
|
|
25
|
+
```php
|
|
26
|
+
<?php
|
|
27
|
+
/**
|
|
28
|
+
* Plugin Name: Awesomate Form Forwarder
|
|
29
|
+
* Description: Relays site form submissions server-side to an n8n webhook.
|
|
30
|
+
*/
|
|
31
|
+
add_action('rest_api_init', function () {
|
|
32
|
+
register_rest_route('awm/v1', '/form/signup', [
|
|
33
|
+
'methods' => 'POST',
|
|
34
|
+
'permission_callback' => '__return_true',
|
|
35
|
+
'callback' => function (WP_REST_Request $req) {
|
|
36
|
+
// Honeypot: bots fill every field; humans never see this one.
|
|
37
|
+
if (!empty($req->get_param('company_website'))) {
|
|
38
|
+
return new WP_REST_Response(['ok' => true], 200); // silent drop
|
|
39
|
+
}
|
|
40
|
+
// Nonce from the page keeps external scripts out.
|
|
41
|
+
if (!wp_verify_nonce($req->get_param('awm_nonce'), 'awm_form_signup')) {
|
|
42
|
+
return new WP_REST_Response(['error' => 'Session expired — reload the page'], 403);
|
|
43
|
+
}
|
|
44
|
+
$email = sanitize_email($req->get_param('email'));
|
|
45
|
+
$name = sanitize_text_field($req->get_param('name'));
|
|
46
|
+
if (!is_email($email) || $name === '') {
|
|
47
|
+
return new WP_REST_Response(['error' => 'Please fill in all fields'], 422);
|
|
48
|
+
}
|
|
49
|
+
$resp = wp_remote_post('https://SLUG.awesomate.io/webhook/signup-v1', [
|
|
50
|
+
'timeout' => 15,
|
|
51
|
+
'headers' => ['Content-Type' => 'application/json'],
|
|
52
|
+
'body' => wp_json_encode([
|
|
53
|
+
'name' => $name, 'email' => $email,
|
|
54
|
+
'page' => esc_url_raw($req->get_header('referer') ?: ''),
|
|
55
|
+
'submitted_at' => gmdate('c'),
|
|
56
|
+
]),
|
|
57
|
+
]);
|
|
58
|
+
if (is_wp_error($resp) || wp_remote_retrieve_response_code($resp) >= 400) {
|
|
59
|
+
return new WP_REST_Response(['error' => 'Could not submit right now — please email us directly'], 502);
|
|
60
|
+
}
|
|
61
|
+
return new WP_REST_Response(['ok' => true], 200);
|
|
62
|
+
},
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Endpoint: `POST {site}/wp-json/awm/v1/form/signup`. One route per form —
|
|
68
|
+
duplicate the `register_rest_route` block with a new path + webhook.
|
|
69
|
+
|
|
70
|
+
The nonce must be printed into the page. Add alongside the form (per-builder
|
|
71
|
+
below) or via `wp_footer`:
|
|
72
|
+
|
|
73
|
+
```php
|
|
74
|
+
add_action('wp_footer', function () {
|
|
75
|
+
echo '<script>window.AWM_FORM_NONCE = "' . wp_create_nonce('awm_form_signup') . '";</script>';
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 2. The form, per builder
|
|
80
|
+
|
|
81
|
+
Detect the builder first: `wp plugin list` / `wp theme list` (via the
|
|
82
|
+
hosting skill). Style with the site's existing design tokens — inspect an
|
|
83
|
+
existing page's buttons/inputs and match.
|
|
84
|
+
|
|
85
|
+
**Bricks**: build a native Bricks form element (fields: name, email; plus a
|
|
86
|
+
hidden `company_website` honeypot field). Bricks' built-in submit actions
|
|
87
|
+
don't reliably POST custom JSON — attach a small custom JS snippet that
|
|
88
|
+
intercepts submit and `fetch()`es the forwarder.
|
|
89
|
+
|
|
90
|
+
**Elementor Pro**: Form widget → Actions After Submit → **Webhook** → the
|
|
91
|
+
FORWARDER URL (not the n8n URL). Add a hidden honeypot field. Nonce: Elementor's
|
|
92
|
+
webhook action can't send it — either relax the nonce check to same-origin
|
|
93
|
+
referer validation for Elementor sites, or use the custom-JS submit instead.
|
|
94
|
+
|
|
95
|
+
**Fallback (no pro form plugin)**: HTML block with a hand-built form +
|
|
96
|
+
`fetch()`:
|
|
97
|
+
|
|
98
|
+
```html
|
|
99
|
+
<form id="awm-signup" novalidate>
|
|
100
|
+
<label>Name <input name="name" required></label>
|
|
101
|
+
<label>Email <input name="email" type="email" required></label>
|
|
102
|
+
<input name="company_website" tabindex="-1" autocomplete="off"
|
|
103
|
+
style="position:absolute;left:-9999px" aria-hidden="true">
|
|
104
|
+
<button type="submit">Sign up</button>
|
|
105
|
+
<p class="awm-msg" role="status"></p>
|
|
106
|
+
</form>
|
|
107
|
+
<script>
|
|
108
|
+
document.getElementById('awm-signup').addEventListener('submit', async (e) => {
|
|
109
|
+
e.preventDefault();
|
|
110
|
+
const form = e.target, btn = form.querySelector('button'), msg = form.querySelector('.awm-msg');
|
|
111
|
+
btn.disabled = true; btn.textContent = 'Sending…';
|
|
112
|
+
const data = Object.fromEntries(new FormData(form));
|
|
113
|
+
data.awm_nonce = window.AWM_FORM_NONCE || '';
|
|
114
|
+
try {
|
|
115
|
+
const r = await fetch('/wp-json/awm/v1/form/signup', {
|
|
116
|
+
method: 'POST', headers: {'Content-Type': 'application/json'},
|
|
117
|
+
body: JSON.stringify(data),
|
|
118
|
+
});
|
|
119
|
+
const body = await r.json();
|
|
120
|
+
if (r.ok) { form.style.display = 'none'; msg.textContent = 'Thanks — you\'re signed up!'; }
|
|
121
|
+
else { msg.textContent = body.error || 'Something went wrong — try again.'; btn.disabled = false; btn.textContent = 'Sign up'; }
|
|
122
|
+
} catch { msg.textContent = 'Network error — please try again.'; btn.disabled = false; btn.textContent = 'Sign up'; }
|
|
123
|
+
});
|
|
124
|
+
</script>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
UX bar for every variant: disabled button + label change while submitting,
|
|
128
|
+
inline thank-you state (swap the form out), and an error state that gives a
|
|
129
|
+
fallback contact method.
|
|
130
|
+
|
|
131
|
+
## 3. End-to-end verification (both hops, in order)
|
|
132
|
+
|
|
133
|
+
1. n8n hop alone: `awesomate_n8n_test` against the workflow — execution
|
|
134
|
+
success, node-by-node inspected.
|
|
135
|
+
2. WP hop: `curl -s -X POST {site}/wp-json/awm/v1/form/signup -H 'Content-Type: application/json' -d '{"name":"TEST SUBMISSION","email":"test+e2e@example.com","awm_nonce":"..."}'`
|
|
136
|
+
(mint a nonce via wp-cli: `wp eval 'echo wp_create_nonce("awm_form_signup");'`).
|
|
137
|
+
3. Confirm a NEW execution appeared for the workflow and the email arrived
|
|
138
|
+
(`[TEST]` subject, user's own inbox).
|
|
139
|
+
4. Honeypot check: same curl + `"company_website":"http://spam.example"` →
|
|
140
|
+
200 ok but NO new execution.
|
|
141
|
+
5. Hand the user: page URL, forwarder endpoint, workflow URL, and where the
|
|
142
|
+
submissions go.
|
|
143
|
+
|
|
144
|
+
## 4. When the workflow is upgraded later
|
|
145
|
+
|
|
146
|
+
The forwarder points at the webhook PATH, and promote preserves it — a
|
|
147
|
+
promoted upgrade needs NO WordPress change. Only if a workflow is rebuilt
|
|
148
|
+
under a new path does the forwarder's `wp_remote_post` URL need updating
|
|
149
|
+
(one line, then re-run step 2-3 above).
|