@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.
@@ -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. Estimates the size of the job in the user's terms BEFORE any `add` call:
11
- ~200 videos × 20–40 min ≈ 70–130 media-hours, i.e. transcription cost in
12
- the tens of dollars AND far beyond the monthly media allowance from
13
- `awesomate_knowledge_status` (checked, not assumed).
14
- 2. States plainly that this will need Ingestion Packs (1 credit = $100
15
- each) beyond the included allowance, and asks for explicit approval —
16
- ideally proposing a cheaper first slice (e.g. the 10–20 most important
17
- videos) as an alternative.
18
- 3. Queues NOTHING until the user says yes. If the user approves, sources
19
- are added via `awesomate_knowledge_sources {action:'add'}` and job
20
- status is reported honestly (media takes a while).
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 the YouTube videos need Pro, using the refusal
20
- or upgrade copy the tool returned. Never an invented price or link.
21
- 4. Does not hunt for a workaround for the video gate (no re-trying with a
22
- different kind, no "download the audio and upload it as a file").
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; extraction skips pages under ~200 chars of real text |
12
- | Whole site / blog | `add {sitemap, since?}` | sitemap.xml; default cap 500 pages per job — a bigger sitemap FAILS the job loudly rather than truncating silently. `since` skips entries with older `<lastmod>` |
13
- | YouTube / hosted video, podcast audio | `add {url}` | **Not transcribed today.** A YouTube link is refused ("YouTube links are not supported yet"); another link to a recording is read as a web page (title and description). Pro and above only; below Pro, or in a trial, it is refused (403 `media_ingest`). To transcribe a recording, upload the file |
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, tagging them, recording that they asked to stop getting email, starting
5
- an email series, reading and writing the account's own records, giving a task, holding a run for a
6
- person's OK, reading the business details and map, and asking the Knowledge Base. It signs in with a
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`; Task
28
- needs `tasks`. Hold for a Decision also needs decisions switched on for the account: until they are,
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 — it
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** — same-origin from the browser; WP→n8n is server-to-server.
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 — webhook trigger with explicit path, e.g.
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
- ## 1. The forwarder (mu-plugin — survives theme changes)
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 — replace the webhook
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 — reload the page'], 403);
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 — please email us directly'], 502);
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 — inspect an
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 — attach a small custom JS snippet that
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 — either relax the nonce check to same-origin
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.querySelector('.awm-msg');
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) { 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'; }
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 email arrived
139
- (`[TEST]` subject, user's own inbox).
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