@awesomate/hosting-mcp 0.49.0 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +44 -4
- package/package.json +1 -1
- package/skill/CHANGELOG.json +16 -0
- package/skill/awesomate-portals/SKILL.md +150 -0
package/dist/index.js
CHANGED
|
@@ -41217,6 +41217,7 @@ var TITLES = {
|
|
|
41217
41217
|
awesomate_crm_apps: "Set up sign-in for your own apps",
|
|
41218
41218
|
awesomate_crm_app_users: "Manage the people who sign in to your apps",
|
|
41219
41219
|
awesomate_crm_assistant: "Set up the AI in your app's conversations",
|
|
41220
|
+
awesomate_crm_hooks: "Send record changes to your n8n",
|
|
41220
41221
|
awesomate_crm_email: "Draft an email to a contact list"
|
|
41221
41222
|
};
|
|
41222
41223
|
function annotate(name, kind) {
|
|
@@ -42886,13 +42887,14 @@ server.registerTool(
|
|
|
42886
42887
|
"awesomate_crm_apps",
|
|
42887
42888
|
{
|
|
42888
42889
|
annotations: annotate("awesomate_crm_apps", MUTATING),
|
|
42889
|
-
description: "The account's own apps (a client portal, a members' app) whose users sign in by email link and then read and change only what each kind's access rules allow them (awesomate_crm_kinds set_access). Pro and above to create or change; reading is every plan. action 'list': each app with its publishable key, allowed origins, sign-up mode and default role. action 'create' {name, allowed_origins, sign_up?, default_role?}: name appears in the sign-in email; allowed_origins are the exact origins the app runs on (https://portal.example.com; http://localhost:5173 for development; capacitor://localhost for the mobile shell), with no path and no wildcard; sign_up 'invite' (default: only people added with awesomate_crm_app_users) or 'open' (anyone who uses the email link gets the default role). action 'update' {app_id, ...any of those, status?}: status 'disabled' signs everyone out of that app and stops its server keys. The publishable key (pk_...) is not a secret: it goes in the app's browser code, with @awesomate/sdk. Never put the account's own token (amt_pat_...) in browser code. Confirm origins and sign-up mode with the user: open sign-up lets strangers in under the default role. SERVER KEYS (ak_...), for the app's own server or an n8n workflow, used with @awesomate/sdk createClient({ token: <key> }) or as a Bearer token on https://hub.awesomate.ai/api/sdk/v1/server: action 'keys' {app_id} lists them (never the secrets); 'create_key' {app_id, name, access: 'read'|'write', kinds?} makes one (kinds leaves it to those kinds, 'contact' to read contacts; leave it out for every kind) and returns the key ONCE: put it straight into the app server's environment (awesomate_app_set_env for an app hosted with us, or the n8n credential) and never into browser code, a repository or a reply to the user; 'revoke_key' {app_id, key_id} stops it at once. A key reads every attribute of an app kind, contacts only as you read them, and writes need Support Plus and above. Prefer the narrowest key: read unless the server writes, and only the kinds it uses.",
|
|
42890
|
+
description: "The account's own apps (a client portal, a members' app) whose users sign in by email link and then read and change only what each kind's access rules allow them (awesomate_crm_kinds set_access). Pro and above to create or change; reading is every plan. action 'list': each app with its publishable key, allowed origins, sign-up mode and default role. action 'create' {name, allowed_origins, sign_up?, default_role?}: name appears in the sign-in email; allowed_origins are the exact origins the app runs on (https://portal.example.com; http://localhost:5173 for development; capacitor://localhost for the mobile shell), with no path and no wildcard; sign_up 'invite' (default: only people added with awesomate_crm_app_users) or 'open' (anyone who uses the email link gets the default role). action 'update' {app_id, ...any of those, status?}: status 'disabled' signs everyone out of that app and stops its server keys. The publishable key (pk_...) is not a secret: it goes in the app's browser code, with @awesomate/sdk. Never put the account's own token (amt_pat_...) in browser code. Confirm origins and sign-up mode with the user: open sign-up lets strangers in under the default role. action 'customer_lookup' {app_id, roles?}: which roles in that app may look customers up (name, email and phone, by search or id, with @awesomate/sdk lookupCustomers) and so put a record under any customer, not only themselves. Without roles it reads the setting; roles ['staff'] switches it on for staff; [] switches it off. Never the app's default role (the one customers sign in with): that would show every customer to every other customer, and the hub refuses it. Off until the owner asks for it. SERVER KEYS (ak_...), for the app's own server or an n8n workflow, used with @awesomate/sdk createClient({ token: <key> }) or as a Bearer token on https://hub.awesomate.ai/api/sdk/v1/server: action 'keys' {app_id} lists them (never the secrets); 'create_key' {app_id, name, access: 'read'|'write', kinds?} makes one (kinds leaves it to those kinds, 'contact' to read contacts; leave it out for every kind) and returns the key ONCE: put it straight into the app server's environment (awesomate_app_set_env for an app hosted with us, or the n8n credential) and never into browser code, a repository or a reply to the user; 'revoke_key' {app_id, key_id} stops it at once. A key reads every attribute of an app kind, contacts only as you read them, and writes need Support Plus and above. Prefer the narrowest key: read unless the server writes, and only the kinds it uses.",
|
|
42890
42891
|
inputSchema: {
|
|
42891
|
-
action: external_exports.enum(["list", "create", "update", "keys", "create_key", "revoke_key"]),
|
|
42892
|
-
app_id: external_exports.string().max(48).optional().describe("update, keys, create_key, revoke_key: the app"),
|
|
42892
|
+
action: external_exports.enum(["list", "create", "update", "keys", "create_key", "revoke_key", "customer_lookup"]),
|
|
42893
|
+
app_id: external_exports.string().max(48).optional().describe("update, keys, create_key, revoke_key, customer_lookup: the app"),
|
|
42893
42894
|
access: external_exports.enum(["read", "write"]).optional().describe("create_key"),
|
|
42894
42895
|
kinds: external_exports.array(external_exports.string().max(41)).max(50).optional().describe("create_key: only these kinds (contact to read contacts); leave out for every kind"),
|
|
42895
42896
|
key_id: external_exports.string().max(64).optional().describe("revoke_key: from keys"),
|
|
42897
|
+
roles: external_exports.array(external_exports.string().max(31)).max(10).optional().describe("customer_lookup: the roles that may look customers up; [] switches it off; leave out to read"),
|
|
42896
42898
|
name: external_exports.string().max(80).optional(),
|
|
42897
42899
|
allowed_origins: external_exports.array(external_exports.string().max(200)).max(10).optional(),
|
|
42898
42900
|
sign_up: external_exports.enum(["invite", "open"]).optional(),
|
|
@@ -42900,10 +42902,15 @@ server.registerTool(
|
|
|
42900
42902
|
status: external_exports.enum(["active", "disabled"]).optional().describe("update only")
|
|
42901
42903
|
}
|
|
42902
42904
|
},
|
|
42903
|
-
async ({ action, app_id, name, allowed_origins, sign_up, default_role, status, access, kinds, key_id }) => {
|
|
42905
|
+
async ({ action, app_id, name, allowed_origins, sign_up, default_role, status, access, kinds, key_id, roles }) => {
|
|
42904
42906
|
try {
|
|
42905
42907
|
const cfg = requireConfig();
|
|
42906
42908
|
if (action === "list") return textResult(await hubGet(cfg, "/api/my-crm/v1/apps"));
|
|
42909
|
+
if (action === "customer_lookup") {
|
|
42910
|
+
if (!app_id) return errorResult(new Error("customer_lookup needs app_id, from list."));
|
|
42911
|
+
const path = `/api/my-crm/v1/apps/${encodeURIComponent(app_id)}/customer-lookup`;
|
|
42912
|
+
return textResult(roles === void 0 ? await hubGet(cfg, path) : await hubPut(cfg, path, { roles }));
|
|
42913
|
+
}
|
|
42907
42914
|
if (action === "keys" || action === "create_key" || action === "revoke_key") {
|
|
42908
42915
|
if (!app_id) return errorResult(new Error(`${action} needs app_id, from list.`));
|
|
42909
42916
|
const keys = `/api/my-crm/v1/apps/${encodeURIComponent(app_id)}/keys`;
|
|
@@ -42992,6 +42999,39 @@ server.registerTool(
|
|
|
42992
42999
|
}
|
|
42993
43000
|
}
|
|
42994
43001
|
);
|
|
43002
|
+
server.registerTool(
|
|
43003
|
+
"awesomate_crm_hooks",
|
|
43004
|
+
{
|
|
43005
|
+
annotations: annotate("awesomate_crm_hooks", MUTATING),
|
|
43006
|
+
description: "Send changes to an app's records to the account's own n8n: when a record of a named kind is created, updated or archived, the hub POSTs an event to a Webhook node on THEIR n8n (no other host is accepted), in order, at least once, retrying while n8n is down. Every writer counts: the app's people, its server key, Claude, a recipe, the assistant. Use it for 'when a job is requested, draft a quote', then write the result back with an app server key (awesomate_crm_apps create_key) from the same workflow. Event body: {id (stable: drop repeats), event: created|updated|archived, kind, record_id, changed: [attributes and links that changed], by, at, record (the record as it is when sent; null once archived), app}. Pro and above to change; reading is every plan. action 'list' {app_id}: each hook with delivered/missed counts and its last error. 'create' {app_id, name, url, kinds, events?}: url is the PRODUCTION webhook URL of an active workflow on their n8n (create and activate the workflow first with the awesomate_n8n tools); returns a secret ONCE: put it straight into an n8n Header Auth credential on that Webhook node (header X-Awesomate-Webhook-Secret), never into a reply. A new hook starts from now. 'update' {app_id, hook_id, name?, url?, kinds?, events?, enabled?}: enabled true retries at once. 'remove' {app_id, hook_id}. 'test' {app_id, hook_id}: one test event now ({event: 'test'}), and what n8n answered. Confirm the kinds and what the workflow will do with the user before creating one.",
|
|
43007
|
+
inputSchema: {
|
|
43008
|
+
action: external_exports.enum(["list", "create", "update", "remove", "test"]),
|
|
43009
|
+
app_id: external_exports.string().max(48).describe("the app, from awesomate_crm_apps list"),
|
|
43010
|
+
hook_id: external_exports.string().max(64).optional().describe("update, remove, test: from list"),
|
|
43011
|
+
name: external_exports.string().max(80).optional(),
|
|
43012
|
+
url: external_exports.string().max(500).optional().describe("create/update: the production URL of a Webhook node on the account's own n8n"),
|
|
43013
|
+
kinds: external_exports.array(external_exports.string().max(41)).max(20).optional(),
|
|
43014
|
+
events: external_exports.array(external_exports.enum(["created", "updated", "archived"])).max(3).optional().describe("default all three"),
|
|
43015
|
+
enabled: external_exports.boolean().optional().describe("update only")
|
|
43016
|
+
}
|
|
43017
|
+
},
|
|
43018
|
+
async ({ action, app_id, hook_id, name, url, kinds, events, enabled }) => {
|
|
43019
|
+
try {
|
|
43020
|
+
const cfg = requireConfig();
|
|
43021
|
+
const path = `/api/my-crm/v1/apps/${encodeURIComponent(app_id)}/hooks`;
|
|
43022
|
+
if (action === "list") return textResult(await hubGet(cfg, path));
|
|
43023
|
+
const fields = Object.fromEntries(Object.entries({ name, url, kinds, events }).filter(([, v]) => v !== void 0));
|
|
43024
|
+
if (action === "create") return textResult(await hubPost(cfg, path, fields));
|
|
43025
|
+
if (!hook_id) return errorResult(new Error(`${action} needs hook_id, from list.`));
|
|
43026
|
+
const one = `${path}/${encodeURIComponent(hook_id)}`;
|
|
43027
|
+
if (action === "remove") return textResult(await hubDelete(cfg, one));
|
|
43028
|
+
if (action === "test") return textResult(await hubPost(cfg, `${one}/test`, {}));
|
|
43029
|
+
return textResult(await hubPatch(cfg, one, { ...fields, ...enabled === void 0 ? {} : { enabled } }));
|
|
43030
|
+
} catch (err) {
|
|
43031
|
+
return errorResult(err);
|
|
43032
|
+
}
|
|
43033
|
+
}
|
|
43034
|
+
);
|
|
42995
43035
|
var crmParam = external_exports.object({
|
|
42996
43036
|
name: external_exports.string().max(41).describe("lower_case"),
|
|
42997
43037
|
type: external_exports.enum(["text", "number", "date", "datetime", "boolean", "uuid", "text_list", "number_list"]).optional().describe("default text"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awesomate/hosting-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.0",
|
|
4
4
|
"description": "Awesomate MCP server — lets Claude manage your Awesomate WordPress hosting, plan, limits, n8n automations, and build Node/static apps + databases",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
package/skill/CHANGELOG.json
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"versions": [
|
|
3
|
+
{
|
|
4
|
+
"version": "0.51.0",
|
|
5
|
+
"highlights": [
|
|
6
|
+
"Your app's staff can now find a customer and put a job under them, straight from the portal. Switch it on per app for the roles that need it (awesomate_crm_apps, customer_lookup). Pro and above",
|
|
7
|
+
"They see name, email and phone only, never a field you marked sensitive, and never the role your customers sign in with: customers still see only themselves"
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.50.0",
|
|
12
|
+
"highlights": [
|
|
13
|
+
"Your n8n can now hear about your app's records (awesomate_crm_hooks): when a job is requested, changed or archived, a workflow runs straight away, with the record attached",
|
|
14
|
+
"Events arrive in order and keep coming back while n8n is down, so a quiet weekend doesn't lose anything. Each has an id, so a workflow can ignore a repeat",
|
|
15
|
+
"Only your own n8n can receive them, protected by a secret your Webhook node checks. Pair it with an app server key to write the result back, such as a drafted quote",
|
|
16
|
+
"New skill, awesomate-portals: ask Claude for a customer portal and it designs the records, who sees what, sign-in by email link and the page, step by step"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
3
19
|
{
|
|
4
20
|
"version": "0.49.0",
|
|
5
21
|
"highlights": [
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: awesomate-portals
|
|
3
|
+
description: Build a portal or members' app on the account's own data with @awesomate/sdk. Customers sign in by email link and see only their own records, staff see everything, lists update live, and optionally an AI assistant answers in conversations. Use when the user says "customer portal", "client login", "members' area", "let my customers see their jobs / bookings / orders", "staff dashboard", "chat with customers", "portal", or wants people outside the business to sign in to something of theirs. Companion to awesomate-app-builder (hosting the page), awesomate-database and awesomate-n8n: same connection, same token.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Awesomate Portals: people sign in and see their own records
|
|
7
|
+
|
|
8
|
+
A portal is three things the account already has, put together: **kinds** (the app's own tables,
|
|
9
|
+
in the account's own database), **access rules** on each kind (who sees and changes what, enforced
|
|
10
|
+
by the database itself), and an **app** (where it runs, how people sign in). The page uses
|
|
11
|
+
`@awesomate/sdk` straight from the browser. There is no app server to write.
|
|
12
|
+
|
|
13
|
+
Work in plain words with the owner. They describe their business ("customers request jobs, we
|
|
14
|
+
quote, they see the status"), you turn that into kinds and rules, show them the design in a
|
|
15
|
+
sentence or two, and only then create anything. Kinds, rules and apps are part of their data,
|
|
16
|
+
not scratch space.
|
|
17
|
+
|
|
18
|
+
## 0. First, every time
|
|
19
|
+
|
|
20
|
+
1. `awesomate_whoami`: everything below acts on that account. Fix the connection if the slug is
|
|
21
|
+
not the one the user means.
|
|
22
|
+
2. `awesomate_get_context`: check the plan. **Sign-in for an app's people is Pro and above.**
|
|
23
|
+
Creating kinds and writing data is Support Plus and above. Reading is every plan. A 403
|
|
24
|
+
carries the upgrade path: relay it honestly, never retry it.
|
|
25
|
+
|
|
26
|
+
## 1. Understand who signs in and what they do
|
|
27
|
+
|
|
28
|
+
Ask, one or two at a time:
|
|
29
|
+
- Who signs in? Usually **customers** (role `member`) and **staff** (role `staff`).
|
|
30
|
+
- What does a customer see? ("their own jobs, and the messages on them")
|
|
31
|
+
- What can a customer do? ("request a job, send a message")
|
|
32
|
+
- What do staff do? ("see every job, set status and quote, reply")
|
|
33
|
+
- Who may sign up: only people the owner adds (`invite`, the default and the safe choice), or
|
|
34
|
+
anyone with the link (`open`, everyone gets the default role)?
|
|
35
|
+
|
|
36
|
+
## 2. Design the kinds
|
|
37
|
+
|
|
38
|
+
`awesomate_crm_kinds {action:'define'}`. Typical portal:
|
|
39
|
+
|
|
40
|
+
| Kind | Attributes | Links |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `job` | `title` (text, required), `status` (choice), `quote` (money) | `customer` to `contact` |
|
|
43
|
+
| `message` | `body` (long_text, required) | `job` to `job` (required) |
|
|
44
|
+
|
|
45
|
+
- **Link the thing to the customer's contact.** That link is what "their own" means.
|
|
46
|
+
- Mark an attribute `readable_by_ai` only when the owner wants Claude and AI assistants to see
|
|
47
|
+
it. Never for anything sensitive. App people see every attribute of a record they may read;
|
|
48
|
+
`readable_by_ai` is only about AI.
|
|
49
|
+
- Show the owner the design ("A job has a title, a status and a quote, and belongs to a
|
|
50
|
+
customer. Messages hang off a job.") and get a yes first.
|
|
51
|
+
|
|
52
|
+
## 3. Set the access rules
|
|
53
|
+
|
|
54
|
+
`awesomate_crm_kinds {action:'set_access'}`. Rules are per kind and per role:
|
|
55
|
+
`all`, `none`, `own` (records they created), or `linked:<path ending at a contact>`.
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
job: {"read": {"member": "linked:customer", "staff": "all"}, "write": {"member": "linked:customer", "staff": "all"}}
|
|
59
|
+
message: {"read": {"member": "linked:job.customer", "staff": "all"}, "write": {"member": "linked:job.customer", "staff": "all"}}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- A role with no rule gets nothing. The database applies these to every read and every write, so
|
|
63
|
+
a mistake in the page can never show anyone more than their role allows.
|
|
64
|
+
- A person can only link a record to something they can see: a customer can point a job at their
|
|
65
|
+
own contact, never someone else's.
|
|
66
|
+
- **Staff putting a job under a customer** needs one more switch, per app: app people read no
|
|
67
|
+
contacts but their own, so by default staff can only link to themselves. Ask the owner, then
|
|
68
|
+
`awesomate_crm_apps {action:'customer_lookup', app_id, roles:['staff']}`. Those roles can then
|
|
69
|
+
search customers (name, email, phone; never a sensitive field) and link a record to any of
|
|
70
|
+
them. Never the app's default role (the one customers sign in with); the hub refuses it.
|
|
71
|
+
|
|
72
|
+
## 4. Create the app and add people
|
|
73
|
+
|
|
74
|
+
1. `awesomate_crm_apps {action:'create', name, allowed_origins, sign_up}`. `allowed_origins` are
|
|
75
|
+
the exact addresses the page runs on (`https://portal.example.com.au`, plus
|
|
76
|
+
`http://localhost:5173` while building). The name appears in the sign-in email. The answer
|
|
77
|
+
has the **publishable key** (`pk_...`): it is not a secret and goes in the page.
|
|
78
|
+
2. `awesomate_crm_app_users {action:'add', email, role}` for each person. A customer is matched to
|
|
79
|
+
their contact by email. Staff get `role: 'staff'`. No email is sent; they ask for a link on the
|
|
80
|
+
page.
|
|
81
|
+
|
|
82
|
+
## 5. Build the page
|
|
83
|
+
|
|
84
|
+
Any static page works (host it with the awesomate-app-builder skill, `kind: 'static'`). The SDK
|
|
85
|
+
loads from a CDN, or from npm in a Vite app:
|
|
86
|
+
|
|
87
|
+
```html
|
|
88
|
+
<script type="module">
|
|
89
|
+
import { createAppClient } from 'https://cdn.jsdelivr.net/npm/@awesomate/sdk@0.8/+esm';
|
|
90
|
+
const app = createAppClient({ publishableKey: 'pk_...' });
|
|
91
|
+
|
|
92
|
+
// The email link signs them in by itself (also when it opens in a tab already on this page).
|
|
93
|
+
app.auth.onChange((user) => (user ? showPortal(user) : showSignIn()));
|
|
94
|
+
app.auth.onSignInError((err) => showMessage(err.message));
|
|
95
|
+
const user = await app.auth.user();
|
|
96
|
+
user ? showPortal(user) : showSignIn();
|
|
97
|
+
|
|
98
|
+
// Sign-in form
|
|
99
|
+
await app.auth.signInWithLink(email, { redirectTo: location.href });
|
|
100
|
+
|
|
101
|
+
// Data: the same calls for everyone; the rules decide what each person gets.
|
|
102
|
+
const stop = app.live('job', { orderBy: ['created_at', 'desc'], limit: 100 }, (rows) => renderJobs(rows));
|
|
103
|
+
await app.write('job', { title: 'Leaking tap', status: 'open' }, { links: { customer: user.contact_id } });
|
|
104
|
+
await app.write('message', { body }, { links: { job: jobId } });
|
|
105
|
+
|
|
106
|
+
// Staff, once customer_lookup is on for their role: find a customer, file a job for them.
|
|
107
|
+
const [customer] = await app.lookupCustomers({ q: 'jo' });
|
|
108
|
+
await app.write('job', { title: 'Gutters', status: 'open' }, { links: { customer: customer.id } });
|
|
109
|
+
</script>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- Choose the view from `user.role` (`staff` sees everything and edit controls; `member` sees
|
|
113
|
+
their own records and a request form).
|
|
114
|
+
- `live()` keeps a list current: anything changed anywhere (staff, Claude, n8n) shows at once.
|
|
115
|
+
- To test customer and staff side by side in one browser, give the staff view its own
|
|
116
|
+
`storageKey` (for example when the address has `?staff`).
|
|
117
|
+
- Serve the HTML with `Cache-Control: no-cache` (an `.htaccess` rule on Awesomate hosting), or
|
|
118
|
+
people keep an old copy of the page after you change it.
|
|
119
|
+
- **Never put the account's token (`amt_pat_...`) or a server key (`ak_...`) in a page.** Only
|
|
120
|
+
the `pk_` key belongs in the browser.
|
|
121
|
+
|
|
122
|
+
## 6. Optional: an AI in the conversations
|
|
123
|
+
|
|
124
|
+
`awesomate_crm_assistant` adds an assistant (for example "Bree") to the message threads. Each
|
|
125
|
+
conversation is Off (people only, the default), Draft (a suggested reply only staff see) or Auto
|
|
126
|
+
(she replies, marked as AI, when she is sure). She reads as the customer she is answering, so
|
|
127
|
+
the access rules bound her, and only `readable_by_ai` attributes reach her. The tool description
|
|
128
|
+
lists the two staff-only kinds it needs (`assistant_thread`, `assistant_draft`) and the
|
|
129
|
+
`from_assistant` attribute on the message kind. Give her a published, public, strict Knowledge
|
|
130
|
+
agent (awesomate-knowledge skill) so she can answer questions about the business. Confirm the
|
|
131
|
+
name and the default mode with the owner: switching it on lets an AI reply to their customers.
|
|
132
|
+
|
|
133
|
+
## 7. Optional: automation
|
|
134
|
+
|
|
135
|
+
- **Server key** (`awesomate_crm_apps {action:'create_key'}`): for the app's own server or an n8n
|
|
136
|
+
workflow to read and write records. Read or write, only the kinds it needs. Shown once: put it
|
|
137
|
+
straight into the server's environment or an n8n credential, never in a reply or a repository.
|
|
138
|
+
- **Record events** (`awesomate_crm_hooks`): when a record of a kind is created, updated or
|
|
139
|
+
archived, the account's own n8n gets an event at once. Build and activate the workflow first
|
|
140
|
+
(awesomate-n8n skill), then create the hook with its production webhook URL and put the secret
|
|
141
|
+
in the Webhook node's Header Auth credential. Use `test` to check it. Example: a job is
|
|
142
|
+
requested, the workflow drafts a quote and writes it back with a write key for `job`.
|
|
143
|
+
|
|
144
|
+
## 8. Check it works
|
|
145
|
+
|
|
146
|
+
- Sign in as a customer and as staff (two windows). The customer must not see another
|
|
147
|
+
customer's records; try it.
|
|
148
|
+
- Write as one, watch the other update live.
|
|
149
|
+
- With an assistant: set one job to Draft, ask a question as the customer, see the draft as staff.
|
|
150
|
+
- Tell the owner what you built in their terms, and what is not built yet.
|