Who you spoke to and how the message must sound. The city and the time of day the e-mail/message(s) go out together drive the sign-off — “Best regards to Amsterdam + have a nice evening”, in German “Beste Grüße nach Amsterdam und einen schönen Abend”.
Create a private app in HubSpot (Settings → Integrations → Private Apps) with the scopes crm.objects.contacts.read and crm.objects.companies.read — plus crm.objects.contacts.write if you want Log in HubSpot to write the internal note back, or Log as Task in HubSpot to put a messenger message on the calendar, since HubSpot files both notes and tasks under the contacts scope. Paste its token here. It is stored in this browser only.
A · The app already runs on Cloudflare Pages from Git — the easy case.
functions/hubspot/[[path]].js is already in the repository. Pages picks up anything under functions/ at the repo root automatically — there is no code editor to find in the dashboard, and no separate Worker to create./hubspot/…, which is same-origin — CORS never even applies.https://<your-domain>/hubspot/crm/v3/objects/contacts. A JSON error about a missing Authorization header is the good answer — it proves the proxy is live. A 404 means the deployment has not picked it up yet.B · Anywhere else — opened from a file, served from localhost, or hosted somewhere without Functions.
hubspot-proxy, then Deploy.deploy/hubspot-proxy-worker.js from this project, then Deploy again.ALLOWED_ORIGINS with the origins that may call it, comma-separated, e.g. https://followup-studio.cxlever.io,http://localhost:8777.https://hubspot-proxy.<subdomain>.workers.dev) into this field.ALLOWED_ORIGINS, separated by commas, and paste the same URL into each app. The Worker holds no token of its own, so nothing is shared between the apps except the route.Already connected a Worker to Git and it will not take variables? Cloudflare then says “Variables cannot be added to a Worker that only has static assets” — wrangler deploy found no wrangler.toml, so it published the repository as files instead of running the script. Route A makes that Worker unnecessary; disconnect or delete it.
Either way the proxy stores nothing and holds no secret. Your private app token travels in the request header from this browser, exactly as it would to HubSpot directly, and the only paths it forwards are reads plus two writes: creating a note and creating a task, both on a contact. A leaked URL can therefore add a note or a task and nothing else — it cannot edit a contact, move a deal or delete anything.
The finished e-mail is pushed straight into your Gmail drafts, formatting and subject already in place — you open Gmail, read it once and hit send. Google requires one thing that cannot be shipped inside an app: a Client ID of your own. It takes about five minutes, once.
Google renamed these screens: what used to be OAuth consent screen is now Google Auth Platform, with its own left-hand menu — Overview, Branding, Audience, Clients, Data Access. The steps below follow that menu.
Follow-Up Studio → Create. Make sure it is the selected project afterwards.Follow-Up Studio, User support email your address → Next. Audience: Internal if this is a Google Workspace account, External for a private gmail.com address → Next. Contact information: your address → Next → agree → Create.gmail.compose, tick .../auth/gmail.compose → Update → Save. That scope creates drafts and cannot read your mail.Follow-Up Studio.https://followup-studio.cxlever.io. Leave Authorised redirect URIs empty → Create..apps.googleusercontent.com) and paste it into the field above.This only works when the app is served over https — Google refuses a file:// origin. On this build the origin to register is —.
Nothing passes through a CxLEVER server: the browser talks to Google directly, the token lives in memory for the length of the session, and Disconnect throws it away.
Paste the actual website/webpage content or scrape(s) into 2 · Inputs below. The URL alone is only a hint for the AI — pasted content is what stops it from inventing things.
The HubSpot import fills both from the contact’s associated companies — the primary company first, the second company after it. With only one company, the second field takes that company’s other URL when its website and domain differ.
The message can only be as deep as what you paste here. Block A is mandatory; B and C are what make the mirror paragraph land — every fact the AI uses from them is listed back to you in the internal note.
The picker uses the same Google Cloud project and the same Client ID as the Gmail draft, so if you have done that, most of this is already behind you. Two more values are needed, both from that same project.
—. API restrictions → Restrict key → tick Google Picker API and Google Drive API. Save.drive.file, tick .../auth/drive.file, Update, then Save.drive.readonly, lets this app read everything in the Drive it is pointed at, and Google classes it as restricted: a project using it cannot leave testing mode without app verification, and a security assessment on top if data crosses a server. drive.file grants nothing until you pick a file, and then grants exactly that file - so there is nothing to verify and nothing to explain in a client's security review. The search you wanted is inside Google's window, and it is Google's own.The preset decides the architecture of the message — which blocks appear, in which order, and how long each piece may be.
The message set as JSON — the file everything else is rendered from. You rarely need to open this: generate below, edit in the preview. It is here for writing a message by hand, for pasting JSON back from an AI, and for reading the error when something does not parse.
Your standing rules. They go into every prompt and every quality check, so the app enforces the same style each time.
Keep your own tone-of-voice prompts here and pick the one this message should follow. The AI receives the profile name, the link and — if you paste it — the text itself. Paste the text: no AI can open your Google Doc from inside this app.
Save snapshot stores a dated copy of the current message set with its quality score. Diff shows exactly what changed against the set you have open now — useful to compare the draft you generated with the one you actually sent. Snapshots live in this browser only.
Self-test of the app's dependencies and current config. No data is sent unless you click “Test connection” above.
hs_write_403 or ai_json_missing, a one-word context label such as note or gemini, and a timestamp. Never a name, never a company, never a transcript, never a message, never a key, never a URL you scraped - the context label is stripped to letters, digits, dots, dashes and underscores before it goes anywhere, so a stray sentence cannot ride along inside it. Only https endpoints are used. It is fire-and-forget: it never retries, and a telemetry failure can never become a failure you see. Leave it off and the app behaves exactly as its privacy note says - nothing leaves this browser but the calls you ask for.Release over one of the three input blocks. Several files at once are fine — each is appended under its own filename.