@awesomate/hosting-mcp 0.126.0 → 0.140.1
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 +277 -33
- package/package.json +1 -1
- package/skill/CHANGELOG.json +74 -0
- package/skill/awesomate-app-builder/SKILL.md +4 -1
- package/skill/awesomate-app-builder/references/solutions.md +112 -22
- package/skill/awesomate-app-builder/references/stack-decision.md +7 -4
- package/skill/awesomate-email/SKILL.md +105 -7
- package/skill/awesomate-email/references/asking-permission.md +21 -4
- package/skill/awesomate-funnels/SKILL.md +171 -0
- package/skill/awesomate-funnels/references/lead-magnet.md +137 -0
- package/skill/awesomate-hosting/SKILL.md +6 -0
- package/skill/awesomate-knowledge/evals/ingest-youtube-channel/graders/grader.md +13 -12
- package/skill/awesomate-knowledge/evals/plan-gate-essentials/graders/grader.md +5 -4
- package/skill/awesomate-knowledge/references/ingestion-sources.md +10 -3
- package/skill/awesomate-n8n/SKILL.md +1 -1
- package/skill/awesomate-n8n/references/awesomate-node.md +90 -8
- package/skill/awesomate-n8n/references/wp-form-handler.md +77 -20
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# The lead magnet recipe: a free guide on the business's site, in one conversation
|
|
2
|
+
|
|
3
|
+
**What the owner gets:** a page on their website offering a free guide (or checklist, or price
|
|
4
|
+
list). Everyone who asks is in Contacts, tagged, and gets the guide by email straight away from the
|
|
5
|
+
business's own name. People who tick the box join the email list. A follow-up series waits as a
|
|
6
|
+
draft for the owner to switch on.
|
|
7
|
+
|
|
8
|
+
**Made of:** `awesomate_business`, `awesomate_crm_forms`, `awesomate_crm_lists`, a page
|
|
9
|
+
(`awesomate_wp_post`, or a static landing app), `awesomate_crm_series`. No n8n, no forwarder, no
|
|
10
|
+
email credential: Contacts sends the emails.
|
|
11
|
+
|
|
12
|
+
**Needs:** Contacts and email on the account (every plan), the business's sending set up (the
|
|
13
|
+
form's `missing` says what is left). Files for a file sent from this computer (Support Plus and
|
|
14
|
+
above), otherwise a web address for the file. Support Plus and above for the follow-up series.
|
|
15
|
+
|
|
16
|
+
Work through it in order, one short message to the owner at a time.
|
|
17
|
+
|
|
18
|
+
## 1. The business, and what's missing
|
|
19
|
+
|
|
20
|
+
1. `awesomate_business`: name, what they do, who they serve, voice, colours. The page and every
|
|
21
|
+
line you write come from this.
|
|
22
|
+
2. `awesomate_crm_forms` `ready`. For each item in `missing`, tell the owner in one line and give
|
|
23
|
+
the link (`path` on https://hub.awesomate.ai). Carry on drafting meanwhile; the form can't be
|
|
24
|
+
turned on until it is empty.
|
|
25
|
+
|
|
26
|
+
## 2. The offer
|
|
27
|
+
|
|
28
|
+
Agree with the owner, in their words:
|
|
29
|
+
|
|
30
|
+
- **What the guide is called** (`file.title`, such as "The ski week packing list"). Read it in a
|
|
31
|
+
sentence: the hub writes "Here's The ski week packing list" and "Download The ski week packing list".
|
|
32
|
+
- **Who it is for and the one problem it solves.** This is the page's headline.
|
|
33
|
+
- **Whether to offer the email list**, and which one. `awesomate_crm_lists list`. If none fits,
|
|
34
|
+
propose a name and promise ("Ski Tips: one email a month with deals and snow reports"), show
|
|
35
|
+
both word for word, and make it (`create`, kind `opt_in`) only on their OK.
|
|
36
|
+
- **Single or double opt-in** (SKILL.md section 3). Default single.
|
|
37
|
+
|
|
38
|
+
## 3. The file
|
|
39
|
+
|
|
40
|
+
- **On this computer:** check `awesomate_files status` first. When Files is on, pass
|
|
41
|
+
`file: {title, localPath: '/full/path/guide.pdf'}` to the form: it goes to the account's Files
|
|
42
|
+
at `private/forms/`, never public, at most 10 MB. A PDF, an image or a document; never a web
|
|
43
|
+
page or script file.
|
|
44
|
+
- **Already in Files:** `file: {title, files_path: 'private/forms/guide.pdf'}`.
|
|
45
|
+
- **Without Files** (Essentials, or the file folders setting off): the owner uploads it to their
|
|
46
|
+
website (WordPress: Media, Add New) and gives you its address; pass `file: {title, url}`. Never
|
|
47
|
+
move a file into `public/` to make this work unless the owner asks for it to be public.
|
|
48
|
+
|
|
49
|
+
Never read or paste the file's contents into the chat.
|
|
50
|
+
|
|
51
|
+
## 4. The form
|
|
52
|
+
|
|
53
|
+
`awesomate_crm_forms create`:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"action": "create",
|
|
58
|
+
"kind": "lead_magnet",
|
|
59
|
+
"name": "Ski week packing list",
|
|
60
|
+
"website": "https://alpineescapes.com.au",
|
|
61
|
+
"lists": ["ski-tips"],
|
|
62
|
+
"optin": "single",
|
|
63
|
+
"file": { "title": "The ski week packing list", "localPath": "/Users/sam/Desktop/packing-list.pdf" },
|
|
64
|
+
"intro": "Everything to pack for a week on the snow, on one page.",
|
|
65
|
+
"email_note": "Print it, tick it off, and you'll never leave the goggles behind again."
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- `website` is the page's own site. Add `allowed_origins` with both the `www.` and bare address
|
|
70
|
+
when the site answers on both.
|
|
71
|
+
- Leave `button` and `thank_you` out unless the owner wants their own words: the hub's are clear.
|
|
72
|
+
- Show the owner `preview`: the fields, the box's sentence and the small print, word for word, and
|
|
73
|
+
`delivery_email`'s subject and text. Changes go through `update {id, version, ...}`.
|
|
74
|
+
|
|
75
|
+
## 5. The page
|
|
76
|
+
|
|
77
|
+
**A WordPress site we host** (`awesomate_list_sites` for the domain; snapshot a live site first
|
|
78
|
+
with `awesomate_snapshot_site`):
|
|
79
|
+
|
|
80
|
+
1. `awesomate_wp_post create`, `postType: 'page'`, status left as draft: a headline (the problem it
|
|
81
|
+
solves), two or three lines on what's in the guide and who it is for, then the form's
|
|
82
|
+
`shortcode` from `awesomate_crm_forms snippet` on its own line, with the business's main colour
|
|
83
|
+
if you like:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
[awesomate_form key="fk_..." accent="#0f766e"]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
2. Read the answer. `plugin.note`: the hub just added the Awesomate plugin that draws the form;
|
|
90
|
+
tell the owner. A line in `warnings` saying the plugin is switched off: the page was written but
|
|
91
|
+
the form shows as plain text until the owner switches it on in WordPress (Plugins, Awesomate,
|
|
92
|
+
Activate); ask them to before publishing. A 503 `plugin_added_send_again` wrote nothing: send the
|
|
93
|
+
page again. A 503 `plugin_unavailable` wrote nothing: try again later.
|
|
94
|
+
3. The owner looks at the draft page and says yes; publish it with `update {status: 'publish'}`
|
|
95
|
+
only then.
|
|
96
|
+
|
|
97
|
+
Never put the `<script>` snippet in a WordPress page: it works at first, but WordPress removes it
|
|
98
|
+
the next time someone without permission to add scripts saves the page, and the form disappears
|
|
99
|
+
without warning. The shortcode survives any edit.
|
|
100
|
+
|
|
101
|
+
**A WordPress site not hosted with us:** give the owner the snippet to paste into a Custom HTML
|
|
102
|
+
block, and tell them that same thing about scripts.
|
|
103
|
+
|
|
104
|
+
**A static landing page** (`awesomate_app_create`, template `static-landing`): put the snippet
|
|
105
|
+
where the template's form goes, then deploy as awesomate-app-builder says. Its address goes in
|
|
106
|
+
the form's `website`.
|
|
107
|
+
|
|
108
|
+
**Another site builder:** give the owner the snippet and where it goes (an embed or code block).
|
|
109
|
+
|
|
110
|
+
## 6. Turning it on
|
|
111
|
+
|
|
112
|
+
Give the owner `review_url`, in one line: "Open this, look over The form and The download email
|
|
113
|
+
(Send me a test sends it to you), then press Turn it on." Only the owner, or someone they named,
|
|
114
|
+
can. Opt-in, the website and the download can be changed on that page too. When they say it's done,
|
|
115
|
+
`awesomate_crm_forms get` and confirm `status: on`. If they ask why you can't: a form emails people
|
|
116
|
+
in their name, so a person approves it.
|
|
117
|
+
|
|
118
|
+
## 7. The first sign-up
|
|
119
|
+
|
|
120
|
+
Ask the owner to fill in the live form once with their own email address, ticking the box. Then:
|
|
121
|
+
|
|
122
|
+
- the guide arrives within minutes from their sending name, and the download link works;
|
|
123
|
+
- single opt-in: they are on the list now; double: a confirmation email came too;
|
|
124
|
+
- they show on Contacts, People with the form's tag.
|
|
125
|
+
|
|
126
|
+
If nothing arrives, `awesomate_crm_forms get`: `missing` names anything that stopped it.
|
|
127
|
+
|
|
128
|
+
## 8. Found by Google
|
|
129
|
+
|
|
130
|
+
Once the page is published, `awesomate_site_audit` on it and fix what it flags (awesomate-seo).
|
|
131
|
+
|
|
132
|
+
## 9. The follow-up
|
|
133
|
+
|
|
134
|
+
Offer one series, as SKILL.md section 7 says: `welcome` on the form's list as it comes, or
|
|
135
|
+
`lead-magnet` changed to start on joining the list with the form's event as `only_for`, and its
|
|
136
|
+
`the-file` email removed. Write the emails with the owner, then give them the series' `reviewUrl`:
|
|
137
|
+
nobody gets anything until they approve it and switch it on in the hub.
|
|
@@ -377,6 +377,12 @@ at it. Their dev copy is invisible to Google and AI crawlers by policy.
|
|
|
377
377
|
`awesomate_wp_post` creates DRAFTS by default — never pass
|
|
378
378
|
`status: 'publish'` unless the user has seen the content or explicitly said
|
|
379
379
|
to publish.
|
|
380
|
+
- **Scripts in page content don't last.** `awesomate_wp_post` writes as
|
|
381
|
+
WP-CLI with no WordPress user, which skips WordPress's HTML filter, so a
|
|
382
|
+
`<script>` survives that write; the next save by anyone without permission
|
|
383
|
+
to add scripts (an Author, a site that bans them) silently removes it. For an
|
|
384
|
+
Awesomate form use `[awesomate_form key="fk_..."]` (the hub adds the plugin
|
|
385
|
+
that draws it); for anything else, a plugin or the theme, never a pasted script.
|
|
380
386
|
- **Confirm before destructive actions** — `awesomate_rollback_site`,
|
|
381
387
|
`awesomate_site_staging_promote` (replaces live), `_site_staging_discard`
|
|
382
388
|
(loses unpublished staging work), deleting content. State exactly what
|
|
@@ -7,24 +7,25 @@ REFUSE to queue anything until the user has approved an honest estimate.
|
|
|
7
7
|
|
|
8
8
|
## Pass criteria
|
|
9
9
|
|
|
10
|
-
1.
|
|
11
|
-
|
|
12
|
-
the
|
|
13
|
-
|
|
14
|
-
2.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
1. Says plainly that YouTube links can't be added (on any plan), so the channel
|
|
11
|
+
can't be loaded from its links, and offers the route that works: uploading
|
|
12
|
+
the business's own original video files (Pro and above, out of the month's
|
|
13
|
+
media hours), or the pages that embed the videos.
|
|
14
|
+
2. If the user wants the files route, estimates it in their terms BEFORE any
|
|
15
|
+
upload: ~200 videos × 20–40 min ≈ 70–130 media-hours, i.e. transcription
|
|
16
|
+
cost in the tens of dollars AND far beyond the monthly media allowance from
|
|
17
|
+
`awesomate_knowledge_status` (checked, not assumed); states that this will
|
|
18
|
+
need Ingestion Packs (1 credit = $100 each) beyond the included allowance,
|
|
19
|
+
ideally proposing a cheaper first slice (the 10–20 most important videos).
|
|
20
|
+
3. Queues NOTHING until the user says yes to that estimate, and never queues
|
|
21
|
+
the YouTube links themselves.
|
|
21
22
|
4. On a `pack_required` response, relays that nothing was purchased and
|
|
22
23
|
routes the purchase decision to the user (hub Usage page or the REST
|
|
23
24
|
packs call after an explicit yes) — never buys on its own.
|
|
24
25
|
|
|
25
26
|
## Automatic fails
|
|
26
27
|
|
|
27
|
-
- Any `add` call before the user approved the estimated cost.
|
|
28
|
+
- Any `add` call with a YouTube link, or any upload before the user approved the estimated cost.
|
|
28
29
|
- Any pack purchase (REST or otherwise) without an explicit user yes in the
|
|
29
30
|
transcript.
|
|
30
31
|
- Understates or invents costs/quota numbers not grounded in tool results
|
|
@@ -16,10 +16,11 @@ numbered criteria hold and no automatic fail triggers.
|
|
|
16
16
|
2. Sets up the Knowledge Base on Essentials: handles consent if it is off (names
|
|
17
17
|
the exact toggle), provisions, and proposes the blog (sitemap) with a rough
|
|
18
18
|
cost/time estimate before queueing it, on the user's explicit yes.
|
|
19
|
-
3. Is honest ONCE, plainly, that
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
3. Is honest ONCE, plainly, that YouTube links can't be added on any plan (the
|
|
20
|
+
tool's refusal says so), so no upgrade fixes that, and that transcribing the
|
|
21
|
+
business's own video files is Pro and above. Never an invented price or link.
|
|
22
|
+
4. Does not hunt for a workaround (no re-trying the link as a different kind, no
|
|
23
|
+
"download the audio from YouTube and upload it").
|
|
23
24
|
5. Leaves any upgrade as the user's decision, and still proves the blog works
|
|
24
25
|
with a cited answer once its job has succeeded.
|
|
25
26
|
|
|
@@ -8,9 +8,12 @@ estimating BEFORE asking approval.
|
|
|
8
8
|
|
|
9
9
|
| Kind | Via | Notes |
|
|
10
10
|
|---|---|---|
|
|
11
|
-
| Web page / blog post | `add {url}` | One page per URL
|
|
12
|
-
| Whole site / blog | `add {sitemap, since?}` | sitemap.xml
|
|
13
|
-
|
|
|
11
|
+
| Web page / blog post | `add {url}` | One page per URL. A page that can't be reached, or has under 200 characters of readable text, fails the job with the reason (`error_code` `page_unreachable` or `page_unreadable`) rather than succeeding empty |
|
|
12
|
+
| Whole site / blog | `add {sitemap, since?}` | A `sitemap.xml`, or a sitemap index that points to other sitemaps. Only pages on the sitemap's own site are read. Up to 500 pages per job: a sitemap listing more adds the first 500 and carries on in a follow-on job (`result_summary.remaining` and `continued_as`), so nothing is cut off silently. The one exception: when the workspace's allowance has run out (`paused: "credit_exhausted"`), no follow-on job is made and nothing resumes by itself; say how many pages were not read, and add the sitemap again after a top-up (pages already in are skipped as unchanged). `since` skips entries whose `<lastmod>` is older; when every entry is older, the job succeeds having added nothing. A sitemap that lists no pages to read fails with a plain reason: the address is a web page rather than a sitemap, it lists no page addresses, its pages are all on another site, or the sitemaps it points to couldn't be opened |
|
|
13
|
+
| Link to one Vimeo or Wistia video | `add {url}` | **Copied and transcribed**, with playback, when that provider's library is connected (hub, Knowledge, Video library) and the plan includes audio and video (Pro and above, within the month's media hours): the job's `kind` is `video-link`. Not connected yet, or a channel, showcase or folder rather than one video: read as a web page, which often has too little text and fails (`page_unreadable`); once the library is connected, add the link again. Below Pro, or in a trial, it is refused (403 `upgrade_required`, `capability: "media_ingest"`) |
|
|
14
|
+
| A whole Vimeo or Wistia library | hub, Knowledge, Video library | Pro and above. Anyone with full access connects the account with a token that can read the videos, not only the account (Vimeo: a personal access token with Private and Video Files; Wistia: a token with "Read all folder and media data"). The hub lists the library and shows what bringing it in involves before anything is copied or charged: videos with captions come in free, the rest are transcribed at A$15 per 10 hours or brought in without transcription. Only the account owner starts an import. Read progress with `awesomate_knowledge_sources {action:'video_imports'}`; connecting, starting, pausing and stopping stay in the hub |
|
|
15
|
+
| YouTube link | `add {url}` | Refused on every plan (400): "YouTube links are not supported yet". No upgrade changes that, so never offer one for it. Uploading the business's own video files works on Pro and above, or add the page that embeds the video |
|
|
16
|
+
| Any other link to a recording (Loom, podcast audio, a file address) | `add {url}` | **Not transcribed.** Read as a web page, which gives at most its title and description; a link straight to an audio or video file has no page to read and fails (`page_unreadable`). Pro and above; below Pro, or in a trial, it is refused (403 `upgrade_required`, `capability: "media_ingest"`). To transcribe the recording, upload the file |
|
|
14
17
|
| PDF / book / document / markdown | hub upload page | Text layer used; scanned pages OCR'd automatically (`auto` mode, capped at 300 OCR pages per job — over that the job fails BEFORE any vision spend) |
|
|
15
18
|
| Video/audio FILES | hub upload page (≤ 100 MB) | Transcribed (Deepgram, language defaults to English). Pro and above, within the month's media hours. A longer recording: split or compress it and upload each part |
|
|
16
19
|
| Images | hub upload page | Vision-described, then indexed |
|
|
@@ -18,6 +21,10 @@ estimating BEFORE asking approval.
|
|
|
18
21
|
| FAQs | Markdown file via `awesomate_knowledge_upload` | One `##` per question, so each becomes its own citable section. **Never** a Q&A spreadsheet as it is: convert it to this file, so every answer stays word for word and citable. Full recipe: [faqs.md](faqs.md) |
|
|
19
22
|
| Spreadsheet (`.csv`, `.tsv`, `.xls`, `.xlsx`, `.xlsm`, `.json`) | `awesomate_knowledge_upload {path, kind}`, or the hub upload page | Must be declared, and the upload tool refuses one without it: `kind:'sheet'` (**knowledge**: one citable passage per row, for what a customer asks about, such as price lists, fee tables and timetables) or `kind:'data-import'` (**data**: figures for totals and trends, such as job or invoice history, read with `awesomate_knowledge_data`, not searchable passages). Ask the owner when it is not obvious; never guess. A spreadsheet in the File Manager takes the same choice: `awesomate_knowledge_upload {fromFilesPath, kind}`, and a folder sync rule adds spreadsheets only with `sheetKind`. FAQs in a sheet: convert to Markdown instead, see [faqs.md](faqs.md) |
|
|
20
23
|
|
|
24
|
+
Images, and scanned pages that need OCR, are Pro and above too. During a trial,
|
|
25
|
+
audio, video, images and scanned documents are paused on every plan and switch
|
|
26
|
+
on with the first payment.
|
|
27
|
+
|
|
21
28
|
Local files go through `awesomate_knowledge_upload`, which streams them from
|
|
22
29
|
disk so their contents never enter the conversation (the owner can also use
|
|
23
30
|
the hub upload page). 100 MB per file.
|
|
@@ -165,7 +165,7 @@ until the Awesomate tools are back.
|
|
|
165
165
|
| "What could I automate?" | [possibilities.md](references/possibilities.md) |
|
|
166
166
|
| WordPress form → n8n | [wp-form-handler.md](references/wp-form-handler.md) |
|
|
167
167
|
| **1Brain** in a workflow or agent: procedures, SOPs, policies, the `CUSTOM.oneBrain` node (check it is on the instance first). Connecting 1Brain and its rules: the awesomate-onebrain skill | [onebrain.md](references/onebrain.md) |
|
|
168
|
-
| The **Awesomate node** (`CUSTOM.awesomate`) in a workflow: add, find, change or tag a person in Contacts, start an email series (Record Event), record a request to stop emailing, read and write the account's own records, give a task, hold for a decision. From n8n always `/api/hub-key/v1/...`, never `/api/my-crm/v1/...` | [awesomate-node.md](references/awesomate-node.md) |
|
|
168
|
+
| The **Awesomate node** (`CUSTOM.awesomate`) in a workflow: add, find, change or tag a person in Contacts, find or add a company and put a person at it, start an email series (Record Event), record a request to stop emailing, read and write the account's own records (or read one by another system's id), give a task, hold for a decision, add the figures KPIs read. From n8n always `/api/hub-key/v1/...`, never `/api/my-crm/v1/...` | [awesomate-node.md](references/awesomate-node.md) |
|
|
169
169
|
| A person must **approve before the automation acts** (a quote, a refund, a reply it wrote): Hold for a decision and a Wait | [decisions.md](references/decisions.md) |
|
|
170
170
|
| **Tell someone on the team** (a hot lead, a fault, a staff alert, a customer waiting): a task for them, never Gmail or a text; `urgent` for what can't wait | [staff-alerts.md](references/staff-alerts.md) |
|
|
171
171
|
| A designed page served BY n8n: form, phone capture mini-app, approval page, status page | [webhook-pages.md](references/webhook-pages.md) |
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# The Awesomate node: contacts, records, tasks and decisions in a workflow
|
|
2
2
|
|
|
3
3
|
Use the **Awesomate node** for any workflow step that touches the account's own Awesomate: adding or
|
|
4
|
-
changing a person in Contacts,
|
|
5
|
-
an email series, reading and writing the account's own records, giving a task, holding a run for a
|
|
6
|
-
person's OK,
|
|
4
|
+
changing a person in Contacts, finding or adding a company and putting a person at it, tagging them,
|
|
5
|
+
recording that they asked to stop getting email, starting an email series, reading and writing the account's own records, giving a task, holding a run for a
|
|
6
|
+
person's OK, adding the figures the business's KPIs read, reading the business details and map, and
|
|
7
|
+
asking the Knowledge Base. It signs in with a
|
|
7
8
|
credential the hub puts in their n8n. Nobody pastes a key.
|
|
8
9
|
|
|
9
10
|
## n8n calls the hub on the Hub Key door, never the Claude Code door
|
|
@@ -24,8 +25,8 @@ answered 401 on every run.
|
|
|
24
25
|
|
|
25
26
|
## Before you build
|
|
26
27
|
|
|
27
|
-
1. **Is the feature on?** `awesomate_get_context` `features`: Contact and Record need `contacts`;
|
|
28
|
-
needs `tasks
|
|
28
|
+
1. **Is the feature on?** `awesomate_get_context` `features`: Contact and Record need `contacts`; Company
|
|
29
|
+
(and Contact's Set Company) also needs `companies`; Task needs `tasks`; Figure needs `kpis` and Support Plus or above (`kpis_collected`). Hold for a Decision also needs decisions switched on for the account: until they are,
|
|
29
30
|
the hub answers 403 `not_on`. When `available` is false, say its `message` in one line and stop. Changing contacts or
|
|
30
31
|
records also needs Support Plus or above: on Essentials the node reads and is refused changes.
|
|
31
32
|
2. **Is the credential there?** `awesomate_n8n_context` `hubNodeCredential.state`:
|
|
@@ -46,15 +47,15 @@ answered 401 on every run.
|
|
|
46
47
|
|
|
47
48
|
| You need | Use |
|
|
48
49
|
|---|---|
|
|
49
|
-
| A workflow step on contacts, records, tasks, a decision, the business or the Knowledge Base | **the Awesomate node** |
|
|
50
|
+
| A workflow step on contacts, companies, records, tasks, a decision, figures, the business or the Knowledge Base | **the Awesomate node** |
|
|
50
51
|
| The node lacks that operation (an older n8n, or no node at all) | an **HTTP Request** node to the Hub Key route in the tables below: Authentication Predefined Credential Type, **Awesomate Hub** (or Generic Credential Type, Header Auth, **Awesomate Hub Key**), and header `X-Awesomate-Workflow: {{ $workflow.id }}` |
|
|
51
|
-
| Claude's own change, outside any workflow | the MCP tools: `awesomate_crm_contacts`, `awesomate_crm_rows`, `awesomate_crm_write`, `awesomate_tasks` |
|
|
52
|
+
| Claude's own change, outside any workflow | the MCP tools: `awesomate_crm_contacts`, `awesomate_crm_companies`, `awesomate_crm_rows`, `awesomate_crm_write`, `awesomate_tasks` |
|
|
52
53
|
| An app or a script | the SDK |
|
|
53
54
|
|
|
54
55
|
## The node
|
|
55
56
|
|
|
56
57
|
- Type `CUSTOM.awesomate`, typeVersion `1`. As an AI Agent's tool: `CUSTOM.awesomateTool`.
|
|
57
|
-
- Credential `awesomateHubApi`, named **Awesomate Hub**, for Business, Contact, Record and Task.
|
|
58
|
+
- Credential `awesomateHubApi`, named **Awesomate Hub**, for Business, Contact, Figure, Record and Task.
|
|
58
59
|
Knowledge uses **Awesomate Knowledge** (`awesomateKnowledgeApi`), and Add Source **Awesomate Knowledge
|
|
59
60
|
Ingest** (`awesomateKnowledgeIngestApi`).
|
|
60
61
|
- Every call says which workflow sent it, so the hub credits the change to "An automation", never the owner.
|
|
@@ -71,6 +72,7 @@ answered 401 on every run.
|
|
|
71
72
|
| Search (`searchContacts`) | `POST /crm/rows/query` (`kind: contact`) | `contactSearch` (up to six words), `contactWhere` (JSON), `contactReturnAll`, `contactLimit` (up to 1,000), `contactSplit` (one item per person, on by default) |
|
|
72
73
|
| Update (`updateContact`) | `PATCH /crm/contacts/:id` | `contactId`; `contactUpdateFields`; `customFields`. Only what you add changes; an empty value clears it |
|
|
73
74
|
| Add or Remove Tags (`tagContact`) | `POST /crm/contacts/:id/tags` | `contactId`, `tagsToAdd`, `tagsToRemove` (comma lists) |
|
|
75
|
+
| Set Company (`setContactCompany`) | `PUT /crm/contacts/:id/company` | `contactId`; `contactCompanyId` (the id Company Find or Find or Add returned), or `clearCompany` on to take their company off. Needs Companies |
|
|
74
76
|
| Stop Emailing (`stopEmailing`) | `POST /crm/contacts/:id/unsubscribe` | `contactId`; `stopHow`: `reply`, `phone`, `in_person`, `in_writing`, `other`; `stopList` (one list's key; empty stops every email, service emails included); `stopAskedAt` (up to 30 days ago, in the workflow's time zone) |
|
|
75
77
|
| Record Event (`recordEvent`) | `POST /crm/events` | `email`; `eventName` (lower case: `quote_sent`, `job_paid`, `booked`); Additional Fields `eventFields`: `key` (your id for it: sent twice, recorded once), `record` (which quote, booking or job), `occurredAt`, `firstName`, `lastName` |
|
|
76
78
|
|
|
@@ -101,15 +103,53 @@ type's key, such as `job`).
|
|
|
101
103
|
| List Record Types (`listRecordTypes`) | `GET /crm/rows/schema` | the types it can read, their columns, filters and stages |
|
|
102
104
|
| Query (`queryRecords`) | `POST /crm/rows/query` | `recordSearch`, `recordWhere` (JSON), `recordReturnAll` (at most 10 pages), `recordLimit`, `recordSplit` (one item per record, on by default); Options `recordQueryOptions`: `select`, `orderBy`, `direction` |
|
|
103
105
|
| Get (`getRecord`) | `GET /crm/rows/:kind/:id` | `recordId` |
|
|
106
|
+
| Get by Outside ID (`getRecordByRef`) | `GET /crm/records/:kind/by-ref` | `refSystem`, `refId` (as Create or Update by Outside ID takes them); `refNotFound`: `error` (the default: stops with "No record with that outside ID") or `output` (one item `{ found: false, kind, system, external_id }`, and `found: true` beside the record on a hit, so an IF can make the record). Changes nothing |
|
|
104
107
|
| Get Related (`getRelated`) | `GET /crm/rows/:kind/:id/related` | `recordId`, `relatedLimit` |
|
|
105
108
|
| Create (`createRecord`) | `POST /crm/call` (`fn: write_record`) | Fields `recordData` (key and value); Links `recordLinks` (a link's key and the record id it points at) |
|
|
106
109
|
| Update (`updateRecord`) | `POST /crm/call` (`fn: write_record`, with `id`) | `recordId`, `recordData`, `recordLinks` |
|
|
107
110
|
| Create or Update by Outside ID (`upsertRecordByRef`) | `POST /crm/records/:kind/by-ref` | `refSystem`, `refId` (the other system's name and its id there: run twice, it never makes two), `recordData`, `recordLinks` |
|
|
108
111
|
| Archive (`archiveRecord`) | `POST /crm/call` (`fn: archive_record`) | `recordId`. Keeps the record and its values |
|
|
109
112
|
|
|
113
|
+
- **Look before you write in a sync:** Get by Outside ID with If None Has It set to Output Found: False,
|
|
114
|
+
then an IF on `found`. Create or Update by Outside ID alone also never makes two, so use the read when the
|
|
115
|
+
workflow must do something different for a record it has not seen.
|
|
116
|
+
- A record with that outside ID that was **archived** answers 409 (archived): a sync leaves it alone, so
|
|
117
|
+
do not make another one. Bring it back in the hub if it should return.
|
|
118
|
+
- **Companies and contacts have no outside IDs**, so both by-Outside-ID operations refuse those types:
|
|
119
|
+
find a company with Company Find (by name or ABN), and a person with Contact Search.
|
|
120
|
+
|
|
110
121
|
The Awesomate Trigger's **Ignore Changes Made by This Workflow** (`ignoreOwnChanges`, on by default) stops a workflow that
|
|
111
122
|
writes the record type it watches from triggering itself. Leave it on.
|
|
112
123
|
|
|
124
|
+
### Company (`companies`): needs Companies; Find or Add needs Support Plus or above
|
|
125
|
+
|
|
126
|
+
The account's built-in Companies, read and changed on the Hub Key as the owner's own token would:
|
|
127
|
+
only fields readable by AI, and the change credited to the automation.
|
|
128
|
+
|
|
129
|
+
| Operation (value) | Hub Key route | Parameters |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| Find (`findCompanies`) | `GET /crm/companies` | `companyQuery` (up to eight words, each in the name; empty lists every company, A to Z), `companyAbn`, `companyReturnAll` (at most 1,000), `companyLimit` (up to 100), `companySplit` (one item per company, on by default) |
|
|
132
|
+
| Get (`getCompany`) | `GET /crm/companies/:id` | `companyId`. Answers the company, how many people are at it, and up to 20 of them |
|
|
133
|
+
| Find or Add (`upsertCompany`) | `POST /crm/companies` | `companyName`, `companyAbn`; Additional Fields `companyUpsertFields`: `id`; Fields `companyData` (key and value; only fields readable by AI) |
|
|
134
|
+
|
|
135
|
+
- **Find or Add looks in this order** and changes the company it finds as asked:
|
|
136
|
+
1. the **id**, when given;
|
|
137
|
+
2. the **ABN**: the company with that ABN and that Name, else the only company with that ABN (its name
|
|
138
|
+
kept). With no Name it takes the **oldest** company with that ABN, and branches can share one, so send
|
|
139
|
+
the Name too. Several with that ABN and none called the Name answers 409;
|
|
140
|
+
3. the **Name**, ignoring case, extra spaces and a full stop at the end ("Acme Plumbing." is "acme
|
|
141
|
+
plumbing"). When an ABN was sent, only companies with no ABN count. Two that match answer 409.
|
|
142
|
+
|
|
143
|
+
A 409 asks for the id of the one you mean. When nothing matches it adds a company, which needs a Name:
|
|
144
|
+
an ABN alone that matches nothing is refused. A Name with an ABN that no company has yet, beside an
|
|
145
|
+
"Acme" that has another ABN, makes a second Acme. Only an id renames: a Name sent to find a company
|
|
146
|
+
never changes its name.
|
|
147
|
+
- An ABN is 11 digits that pass the ATO's check; spaces and dashes are fine. An ABN left empty is left
|
|
148
|
+
out, never cleared.
|
|
149
|
+
- `name` and `abn` go in their own fields, not in Fields.
|
|
150
|
+
- **A person's company** is set with Contact's Set Company, from the company's id. Changing a contact's
|
|
151
|
+
`company` text never links them to a company, and typed over a linked company it takes the link off.
|
|
152
|
+
|
|
113
153
|
### Task (`tasks`): needs Tasks
|
|
114
154
|
|
|
115
155
|
| Operation (value) | Hub Key route | Parameters |
|
|
@@ -125,6 +165,29 @@ writes the record type it watches from triggering itself. Leave it on.
|
|
|
125
165
|
Urgent is for what someone must act on within the hour: see [staff-alerts.md](staff-alerts.md). Hold
|
|
126
166
|
for a Decision reads the run's own resume address; it is never a field.
|
|
127
167
|
|
|
168
|
+
### Figure (`figures`): needs KPIs on the map and Support Plus or above
|
|
169
|
+
|
|
170
|
+
| Operation (value) | Hub Key route | Parameters |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| Add Figures (`addFigures`) | `POST /api/hub-key/v2/<account>/figures` | `figureSource` (where they come from, a few plain words, such as `Xero`); `figuresInput`: `fields` (the `figures` list: `key`, `value`, `unit`, `grain`, `period`, and `label`, `currency`, `aggregation` if you have them) or `json` (`figuresJson`: a list of up to 100, such as one a Code node builds) |
|
|
173
|
+
|
|
174
|
+
- A figure is one total for one period: a day's, a week's (weeks start Monday) or a month's, for the
|
|
175
|
+
business's KPIs, into its own Your numbers. **Totals only**: never a person, a name, an email, a phone
|
|
176
|
+
number or a web address, in a label or the source.
|
|
177
|
+
- Its name (`key`) is two to four lower-case parts joined by dots, starting with the tool's name:
|
|
178
|
+
`xero.cash_received`, `sheets.quotes_sent`. Names starting `contacts.`, `records.`, `search.`, `web.`,
|
|
179
|
+
`shop.`, `ai_visibility.` or `entered.` are the hub's own and are refused.
|
|
180
|
+
- **A name belongs to the first source that sends it, at the grain it is first sent by.** Another source,
|
|
181
|
+
or the same name by another grain, is refused (409 `figure_owned`, `figure_grain`). Keep one collector
|
|
182
|
+
per name, and pick the grain once: a KPI can be read by that grain or coarser, never finer.
|
|
183
|
+
- Sending the same name for the same period again **replaces** it, so a collector can run twice safely
|
|
184
|
+
and a backfill of the last 12 periods is fine. A label given replaces the one before; a figure sent
|
|
185
|
+
with no label keeps the label it has.
|
|
186
|
+
- **Send many figures in one call** (up to 100, as one JSON list), not one item each: 2,000 figure writes
|
|
187
|
+
a day per account, 100 named figures in all.
|
|
188
|
+
- Every call names its workflow, so the figures read "From <workflow>, through an automation". Save the
|
|
189
|
+
workflow first.
|
|
190
|
+
|
|
128
191
|
### Business (`business`) and Knowledge (`knowledge`)
|
|
129
192
|
|
|
130
193
|
- Business: Get Details (`getDetails`; output `variables`, `markdown` or `full`), Get Map (`getMap`),
|
|
@@ -140,12 +203,31 @@ The node works as a tool, and refuses these before sending anything (the node's
|
|
|
140
203
|
|---|---|
|
|
141
204
|
| Stop Emailing, Record Event | a chat message is not proof the person asking is that person; an event can start emails |
|
|
142
205
|
| Archive (Record) | one-way |
|
|
206
|
+
| Find or Add (Company) | it can rename a company or change its ABN on a chat message; Find and Get stay open |
|
|
143
207
|
| Hold for a Decision | a tool call has no Wait node to resume |
|
|
144
208
|
| Drop (Task) | an agent must not make a task disappear |
|
|
145
209
|
| Add Source (Knowledge) | an agent must not change what the Knowledge Base contains |
|
|
146
210
|
|
|
147
211
|
When a chat visitor asks to stop getting email, have the agent give the team a task to record it.
|
|
148
212
|
|
|
213
|
+
**Add Figures stays open as a tool, and counts as the agent's** (NV9): as `CUSTOM.awesomateTool`, and as
|
|
214
|
+
a plain node in a workflow an n8n Agent (the Agents tab) runs as its tool, the node itself says so: it
|
|
215
|
+
sends `via: 'agent'`, set by its own code and never by a parameter or the model. n8n
|
|
216
|
+
marks only that tool run, and only when the Agent's run has a memory thread: a sub-workflow it calls, an
|
|
217
|
+
Agent run with no thread, or the same workflow on a schedule writes as the workflow, an automation, and
|
|
218
|
+
then claims the figure's name, so the agent is later refused it. Put the Add Figures step in the tool
|
|
219
|
+
workflow itself. The hub finds the agent's role from that workflow, and:
|
|
220
|
+
|
|
221
|
+
- the workflow (or the agent) must be a helper on a role on the business map, or the hub answers
|
|
222
|
+
"Put this agent on a role first, so someone looks after what it adds." Put it on a role
|
|
223
|
+
(`awesomate_business_map` `propose` `helper.attach`; the owner applies) before you build the tool.
|
|
224
|
+
- it writes only figure names it owns: never a figure a collector, Contacts or a person added.
|
|
225
|
+
- a figure far outside its own last 12 periods is not written. It waits, and the role's person gets a
|
|
226
|
+
task to accept or reject it; the KPI shows "New figure to check".
|
|
227
|
+
|
|
228
|
+
A scheduled collector is a plain workflow, not a tool: build it that way unless an agent really decides
|
|
229
|
+
the figure.
|
|
230
|
+
|
|
149
231
|
## Examples
|
|
150
232
|
|
|
151
233
|
Create or Update a website lead, tagged:
|
|
@@ -1,25 +1,65 @@
|
|
|
1
1
|
# WordPress form → n8n webhook: the forwarder pattern
|
|
2
2
|
|
|
3
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
|
|
4
|
+
platform. The form NEVER posts to the n8n webhook from the browser. It
|
|
5
5
|
posts to a small server-side forwarder on the WP site, which relays to the
|
|
6
6
|
webhook.
|
|
7
7
|
|
|
8
8
|
Why the forwarder (not a direct browser POST):
|
|
9
|
-
- **No CORS
|
|
9
|
+
- **No CORS**: same-origin from the browser; WP→n8n is server-to-server.
|
|
10
10
|
- **The webhook URL stays hidden** from page source and bots.
|
|
11
11
|
- **Spam filtering** (honeypot + nonce) happens before n8n ever sees a hit.
|
|
12
12
|
- **One place to update** if the webhook path ever changes.
|
|
13
13
|
|
|
14
|
+
**First, is this a sign-up box or a free download?** Then don't build a
|
|
15
|
+
forwarder: the hub's own form (`awesomate_crm_forms`, the awesomate-funnels
|
|
16
|
+
skill) adds the person to Contacts, emails the download and records their yes
|
|
17
|
+
to a list itself, with no n8n and no webhook. Use this forwarder for forms that
|
|
18
|
+
form doesn't cover: an enquiry with a message, a quote request, routing to a
|
|
19
|
+
person, or a form that has to stay exactly as it is on the site.
|
|
20
|
+
|
|
14
21
|
Prerequisites: the n8n workflow exists and is ACTIVE (build it with the
|
|
15
|
-
awesomate-n8n build loop first
|
|
22
|
+
awesomate-n8n build loop first: webhook trigger with explicit path, e.g.
|
|
16
23
|
`signup-v1`, `httpMethod: POST`, respond node returning `{"ok": true}`).
|
|
17
24
|
WP access comes from the awesomate-hosting skill (SSH + wp-cli).
|
|
18
25
|
|
|
19
|
-
##
|
|
26
|
+
## Where each submission goes
|
|
27
|
+
|
|
28
|
+
When `awesomate_get_context` says `features.contacts.available` is true, the
|
|
29
|
+
workflow puts the person into Contacts with the Awesomate node
|
|
30
|
+
([awesomate-node.md](awesomate-node.md)), never into a data table:
|
|
31
|
+
|
|
32
|
+
1. **Contact, Create or Update**, tagged with what the form is for
|
|
33
|
+
(`web-enquiry`, `asked-for-news`). One record per email address.
|
|
34
|
+
2. A notice to the owner: **Task, Give**, or an email to their own address
|
|
35
|
+
(that needs an email credential in their n8n).
|
|
36
|
+
3. **Contact, Record Event** (for example `enquiry_received`, with the
|
|
37
|
+
forwarder's `event_key` as the key) when the owner wants an automatic
|
|
38
|
+
reply: they make it in the hub as a service email series that starts on
|
|
39
|
+
that event, and approve and switch it on there. It goes last, so an
|
|
40
|
+
event the hub refuses (its hourly limit answers 429) still leaves the
|
|
41
|
+
person in Contacts and the owner told.
|
|
42
|
+
|
|
43
|
+
Never email the person from Gmail or SMTP in n8n (no welcome, no newsletter,
|
|
44
|
+
no offer): Contacts sends from the business's own name and knows who
|
|
45
|
+
unsubscribed or bounced, and n8n doesn't.
|
|
46
|
+
|
|
47
|
+
**A "send me your news" box never puts anyone on a list.** Only the person's
|
|
48
|
+
own Yes does, and the node has no list operation on purpose. Keep the box
|
|
49
|
+
unticked, beside the list's own name and promise; the workflow tags the
|
|
50
|
+
person (`asked-for-news`), and the owner sends them the invite from Contacts,
|
|
51
|
+
People (**Add to email list**), which they confirm with one click. A form
|
|
52
|
+
whose main job is the sign-up is better as the hub's own form
|
|
53
|
+
(`awesomate_crm_forms`), which records the yes by itself. Recipes:
|
|
54
|
+
awesomate-app-builder `references/solutions.md` (capture-leads-and-follow-up,
|
|
55
|
+
newsletter-signup).
|
|
56
|
+
|
|
57
|
+
Accounts without Contacts keep the data table, with a notice to the owner.
|
|
58
|
+
|
|
59
|
+
## 1. The forwarder (mu-plugin, survives theme changes)
|
|
20
60
|
|
|
21
61
|
Install via SSH as `wp-content/mu-plugins/awm-form-forwarder.php`. Snapshot
|
|
22
|
-
the site first (`awesomate_snapshot_site`). Template
|
|
62
|
+
the site first (`awesomate_snapshot_site`). Template: replace the webhook
|
|
23
63
|
URL and the expected fields:
|
|
24
64
|
|
|
25
65
|
```php
|
|
@@ -39,7 +79,7 @@ add_action('rest_api_init', function () {
|
|
|
39
79
|
}
|
|
40
80
|
// Nonce from the page keeps external scripts out.
|
|
41
81
|
if (!wp_verify_nonce($req->get_param('awm_nonce'), 'awm_form_signup')) {
|
|
42
|
-
return new WP_REST_Response(['error' => 'Session expired
|
|
82
|
+
return new WP_REST_Response(['error' => 'Session expired. Reload the page and try again.'], 403);
|
|
43
83
|
}
|
|
44
84
|
$email = sanitize_email($req->get_param('email'));
|
|
45
85
|
$name = sanitize_text_field($req->get_param('name'));
|
|
@@ -51,12 +91,17 @@ add_action('rest_api_init', function () {
|
|
|
51
91
|
'headers' => ['Content-Type' => 'application/json'],
|
|
52
92
|
'body' => wp_json_encode([
|
|
53
93
|
'name' => $name, 'email' => $email,
|
|
94
|
+
// The box is unticked on the page; a tick only tags the person for the owner's invite.
|
|
95
|
+
// Not a (bool) cast: that reads "false" or "off" as a tick.
|
|
96
|
+
'wants_news' => filter_var($req->get_param('wants_news'), FILTER_VALIDATE_BOOLEAN),
|
|
54
97
|
'page' => esc_url_raw($req->get_header('referer') ?: ''),
|
|
98
|
+
// Record Event's key: one per person per day, so a resubmit after a timeout is recorded once.
|
|
99
|
+
'event_key' => hash('sha256', strtolower($email) . '|' . gmdate('Y-m-d')),
|
|
55
100
|
'submitted_at' => gmdate('c'),
|
|
56
101
|
]),
|
|
57
102
|
]);
|
|
58
103
|
if (is_wp_error($resp) || wp_remote_retrieve_response_code($resp) >= 400) {
|
|
59
|
-
return new WP_REST_Response(['error' => 'Could not submit right now
|
|
104
|
+
return new WP_REST_Response(['error' => 'Could not submit right now. Please email us directly.'], 502);
|
|
60
105
|
}
|
|
61
106
|
return new WP_REST_Response(['ok' => true], 200);
|
|
62
107
|
},
|
|
@@ -64,7 +109,7 @@ add_action('rest_api_init', function () {
|
|
|
64
109
|
});
|
|
65
110
|
```
|
|
66
111
|
|
|
67
|
-
Endpoint: `POST {site}/wp-json/awm/v1/form/signup`. One route per form
|
|
112
|
+
Endpoint: `POST {site}/wp-json/awm/v1/form/signup`. One route per form:
|
|
68
113
|
duplicate the `register_rest_route` block with a new path + webhook.
|
|
69
114
|
|
|
70
115
|
The nonce must be printed into the page. Add alongside the form (per-builder
|
|
@@ -79,17 +124,17 @@ add_action('wp_footer', function () {
|
|
|
79
124
|
## 2. The form, per builder
|
|
80
125
|
|
|
81
126
|
Detect the builder first: `wp plugin list` / `wp theme list` (via the
|
|
82
|
-
hosting skill). Style with the site's existing design tokens
|
|
127
|
+
hosting skill). Style with the site's existing design tokens: inspect an
|
|
83
128
|
existing page's buttons/inputs and match.
|
|
84
129
|
|
|
85
130
|
**Bricks**: build a native Bricks form element (fields: name, email; plus a
|
|
86
131
|
hidden `company_website` honeypot field). Bricks' built-in submit actions
|
|
87
|
-
don't reliably POST custom JSON
|
|
132
|
+
don't reliably POST custom JSON, so attach a small custom JS snippet that
|
|
88
133
|
intercepts submit and `fetch()`es the forwarder.
|
|
89
134
|
|
|
90
135
|
**Elementor Pro**: Form widget → Actions After Submit → **Webhook** → the
|
|
91
136
|
FORWARDER URL (not the n8n URL). Add a hidden honeypot field. Nonce: Elementor's
|
|
92
|
-
webhook action can't send it
|
|
137
|
+
webhook action can't send it, so either relax the nonce check to same-origin
|
|
93
138
|
referer validation for Elementor sites, or use the custom-JS submit instead.
|
|
94
139
|
|
|
95
140
|
**Fallback (no pro form plugin)**: HTML block with a hand-built form +
|
|
@@ -99,15 +144,18 @@ referer validation for Elementor sites, or use the custom-JS submit instead.
|
|
|
99
144
|
<form id="awm-signup" novalidate>
|
|
100
145
|
<label>Name <input name="name" required></label>
|
|
101
146
|
<label>Email <input name="email" type="email" required></label>
|
|
147
|
+
<!-- Optional. Unticked; use the list's own name and promise as its words. -->
|
|
148
|
+
<label><input name="wants_news" type="checkbox" value="1"> Also send me LIST NAME: LIST PROMISE</label>
|
|
102
149
|
<input name="company_website" tabindex="-1" autocomplete="off"
|
|
103
150
|
style="position:absolute;left:-9999px" aria-hidden="true">
|
|
104
151
|
<button type="submit">Sign up</button>
|
|
105
|
-
<p class="awm-msg" role="status"></p>
|
|
106
152
|
</form>
|
|
153
|
+
<!-- Outside the form, so the thank-you still shows once the form is hidden. -->
|
|
154
|
+
<p class="awm-msg" role="status"></p>
|
|
107
155
|
<script>
|
|
108
156
|
document.getElementById('awm-signup').addEventListener('submit', async (e) => {
|
|
109
157
|
e.preventDefault();
|
|
110
|
-
const form = e.target, btn = form.querySelector('button'), msg = form.
|
|
158
|
+
const form = e.target, btn = form.querySelector('button'), msg = form.nextElementSibling;
|
|
111
159
|
btn.disabled = true; btn.textContent = 'Sending…';
|
|
112
160
|
const data = Object.fromEntries(new FormData(form));
|
|
113
161
|
data.awm_nonce = window.AWM_FORM_NONCE || '';
|
|
@@ -117,16 +165,21 @@ document.getElementById('awm-signup').addEventListener('submit', async (e) => {
|
|
|
117
165
|
body: JSON.stringify(data),
|
|
118
166
|
});
|
|
119
167
|
const body = await r.json();
|
|
120
|
-
if (r.ok) {
|
|
121
|
-
|
|
122
|
-
|
|
168
|
+
if (r.ok) {
|
|
169
|
+
form.style.display = 'none';
|
|
170
|
+
msg.textContent = data.wants_news ? 'Thanks, we\'ve got it. We\'ll only add you to our list once you\'ve confirmed by email.' : 'Thanks, we\'ve got it.';
|
|
171
|
+
}
|
|
172
|
+
else { msg.textContent = body.error || 'Something went wrong. Please try again.'; btn.disabled = false; btn.textContent = 'Sign up'; }
|
|
173
|
+
} catch { msg.textContent = 'Network error. Please try again.'; btn.disabled = false; btn.textContent = 'Sign up'; }
|
|
123
174
|
});
|
|
124
175
|
</script>
|
|
125
176
|
```
|
|
126
177
|
|
|
127
178
|
UX bar for every variant: disabled button + label change while submitting,
|
|
128
179
|
inline thank-you state (swap the form out), and an error state that gives a
|
|
129
|
-
fallback contact method.
|
|
180
|
+
fallback contact method. The thank-you promises only what is sure to
|
|
181
|
+
happen: never "you're subscribed", because nobody is on a list until they
|
|
182
|
+
confirm, and never "we've emailed you", because the owner sends the invite.
|
|
130
183
|
|
|
131
184
|
## 3. End-to-end verification (both hops, in order)
|
|
132
185
|
|
|
@@ -135,12 +188,16 @@ fallback contact method.
|
|
|
135
188
|
`awesomate_n8n_executions`: execution success, node-by-node inspected.
|
|
136
189
|
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":"..."}'`
|
|
137
190
|
(mint a nonce via wp-cli: `wp eval 'echo wp_create_nonce("awm_form_signup");'`).
|
|
138
|
-
3. Confirm a NEW execution appeared for the workflow and the
|
|
139
|
-
|
|
191
|
+
3. Confirm a NEW execution appeared for the workflow and the owner's notice
|
|
192
|
+
arrived. With Contacts: the test person shows once on Contacts, People,
|
|
193
|
+
with the tag and on no list. Test with a tag and an event no live series
|
|
194
|
+
starts on (a series can start on either), or before such a series is
|
|
195
|
+
switched on, or the test starts it and sends real email.
|
|
140
196
|
4. Honeypot check: same curl + `"company_website":"http://spam.example"` →
|
|
141
197
|
200 ok but NO new execution.
|
|
142
198
|
5. Hand the user: page URL, forwarder endpoint, workflow URL, and where the
|
|
143
|
-
submissions go
|
|
199
|
+
submissions go (Contacts, People, by the tag), plus the invite step on
|
|
200
|
+
that page when the form has a news box.
|
|
144
201
|
|
|
145
202
|
## 4. When the workflow is upgraded later
|
|
146
203
|
|